Hochschule Bochum

Hochschule Bochum
Fachbereich Informatik und Ingenieurwissenschaften
Wahlmodul: Projektbasierte Vertiefung aktueller Themen der Informatik
Betreuer: Prof. Dr. rer. nat. Henrik Blunck, Prof. Dr.-Ing. Ralph Lindken, Marc Ladwig (M.Sc.)


Inhaltsverzeichnis¶

  1. Installationsanleitung
    • 1.1 Erzeugung eines virtuellen Environments und Installation der Dependencies
    • 1.2 Paketinstallation in Anaconda mit der Konsole
    • 1.3 Verwendung von JupyterLab
  2. Aufgabenbeschreibung und Datenstruktur
    • 2.1 Beschreibung
    • 2.2 Aufgabe
    • 2.3 Daten
      • 2.3.1 KiDAQ-System Drucksignale
      • 2.3.2 VIB-System Schwingungssignale
    • 2.4 Beschreibung der Rohdatenstruktur
  3. Deskriptive und Explorative Datenanalyse
    • 3.1 Datenanalyse
    • 3.2 Aggregative Analyse
    • 3.3 Frequentative Analyse
      • 3.3.1 Fast-Fourier-Transformation (FFT))
      • 3.3.2 Kurzzeit-Fourier-Transformation (STFT))
      • 3.3.3 Wavelet-Transform
  4. Preprocessing
    • 4.1 Parametrierung
    • 4.2 Start
    • 4.3 Vorbereitung der Tainings- und Testdaten
  5. Maschinelles Lernen
    • 5.1 Trainings- und Testdaten wählen und laden
    • 5.2 Entscheidungsbaum - sklearn (empfohlen))
      • 5.2.1 Konfiguration 1
      • 5.2.2 Trainieren 1
    • 5.3 Tiefes neuronales Netz - tensorflow
      • 5.3.1 Konfiguration 2
      • 5.3.2 Trainieren 2
    • 5.4 Extremes Gradienten-Boosting - XGBoost
      • 5.4.1 Konfiguration 3
      • 5.4.2 Trainieren 3
  6. Modelanalyse des Learners
  7. Statische Interpretation des Resultats
    • 7.1 Deskriptive und Explorative Datenanalyse
      • 7.1.1 Aggregation
      • 7.1.2 Frequenztransformation
        • 7.1.2.1 FFT der Vibrationssignale von KiDAQ und VIB im Vergleich
        • 7.1.2.2 Verwendung und Interpretation von unterschiedlichen spektralen Analysemethoden
    • 7.2 Interpretation des maschinellen Lernens
      • 7.2.1 VIB
      • 7.2.2 KiDAQ

1. Installationsanleitung¶

1.1 Erzeugung eines virtuellen Environments und Installation der Dependencies¶

  1. Öffne innerhalb des Notebook Folders ein Terminal
  2. Erzeuge ein virtuelles Environment mit python3 -m venv venv
  3. Aktiviere das virtuelle Environment mit .\venv\Scripts\activate
  4. Danach siehst du ein (venv) vor deinem Terminal Prompt
  5. Installiere die requirements.txt mit pip install -r requirements.txt

1.2 Paketinstallation in Anaconda mit der Konsole¶

  1. Öffnen Sie das Startmenü und suchen Sie nach "Anaconda Prompt".
  2. Starten Sie die Anaconda Prompt aus den Suchergebnissen. Das Anaconda Prompt-Fenster wird geöffnet.
  3. Installieren Sie ein Paket oder eine Dependency, indem Sie den folgenden Befehl eingeben conda install paketname

Ersetzen Sie paketname durch den Namen des Pakets oder der Dependency, die Sie installieren möchten. Sie können auch mehrere Paketnamen angeben, indem Sie sie durch Leerzeichen trennen.

  1. Warten Sie, bis die Installation abgeschlossen ist. Anaconda wird automatisch alle benötigten Abhängigkeiten herunterladen und installieren.
  2. Überprüfen Sie, ob die Installation erfolgreich war, indem Sie das Paket oder die Dependency verwenden oder den folgenden Befehl ausführen, um eine Liste der installierten Pakete anzuzeigen conda list

Dadurch wird eine Liste aller installierten Pakete in der aktuellen Umgebung angezeigt.

  1. Wiederholen Sie die Schritte 3-5 für alle weiteren Pakete oder Dependencies, die Sie installieren möchten.
  2. Wenn Sie die Installation beendet haben, können Sie die Anaconda Prompt schließen oder mit weiteren Befehlen und Aktionen fortfahren.

1.3 Verwendung von JupyterLab¶

  1. Öffnen Sie das Startmenü und suchen Sie nach "Anaconda Navigator".

  2. Starten Sie den Anaconda Navigator aus den Suchergebnissen. Das Anaconda Navigator-Fenster wird geöffnet.

  3. Klicken Sie auf das Symbol für JupyterLab. Dadurch wird ein neues Browserfenster geöffnet, das die JupyterLab-Benutzeroberfläche anzeigt.

  4. In der JupyterLab-Benutzeroberfläche können Sie entweder eine neue Datei erstellen oder vorhandene Dateien öffnen.

    • Um eine neue Datei zu erstellen, klicken Sie auf das "+" -Symbol in der Symbolleiste und wählen Sie den gewünschten Dateityp, z. B. "Python 3". Dadurch wird eine neue Datei geöffnet.

    • Um eine vorhandene Datei zu öffnen, klicken Sie auf "File" in der Menüleiste und wählen Sie "Open" aus. Navigieren Sie zum Speicherort der Datei auf Ihrem Computer und wählen Sie sie aus. Die Datei wird in einem neuen Tab geöffnet.

  5. Jeder geöffnete Tab enthält eine Arbeitsfläche, in der Sie Code schreiben, Dateien anzeigen und bearbeiten, sowie andere Aktionen durchführen können.

  6. JupyterLab bietet verschiedene Arten von Dokumenten, die Sie in der Arbeitsfläche verwenden können:

    • Jupyter Notebook: Sie können Jupyter Notebook-Dateien (.ipynb) öffnen und bearbeiten. Diese Dateien bestehen aus Zellen, die Code, Text oder Markdown enthalten können.

    • Texteditor: Sie können Textdateien (.txt), Python-Skripte (.py) und andere Textdateiformate anzeigen und bearbeiten.

    • Terminal: Sie können eine Befehlszeilenschnittstelle öffnen und Befehle direkt in JupyterLab ausführen.

  7. Jeder Tab enthält eine Symbolleiste mit verschiedenen Optionen, um Aktionen wie das Ausführen von Code, das Hinzufügen neuer Zellen, das Speichern von Dateien und vieles mehr durchzuführen.

  8. JupyterLab bietet auch eine Vielzahl von Tastenkombinationen, die Ihnen helfen können, schneller zu arbeiten. Sie können auf "Help" in der Menüleiste klicken und dann "Keyboard Shortcuts" auswählen, um eine Liste der verfügbaren Tastenkombinationen anzuzeigen.

  9. Um JupyterLab zu beenden, können Sie das Browserfenster schließen oder zur JupyterLab-Benutzeroberfläche zurückkehren und auf "File" in der Menüleiste klicken, gefolgt von "Quit".


Analyse Pumpenprüfstandsdaten (ML-Workflow)¶

Für die ordnungsgemäße Nutzung müssen einige Bibliotheken (Libraries) vorab importiert werden. Ein Import wird über das Schlüsselwort import durchgeführt, zusätzlich kann noch über das as der Name des importieren Pakets geändert werden.

2. Aufgabenbeschreibung und Datenstruktur¶

2.1 Beschreibung¶

Die Zustandsüberwachung hydraulischer Anlagen und insbesondere Pumpen spielt eine wichtige Rolle bei der Sicherung eines korrekten Betriebs. Durch die Integration von künstlicher Intelligenz in vorrausschauende Systeme gewinnt die Predictive Maintenance gestützt durch künstliche Intelligenz an Bedeutung. Kreiselpumpen sind aufgrund ihrer Verbreitung in Industrie, Gewerbe und Haushalten besonders relevant für eine effiziente Betriebsweise. Schwingungsbasierte Systeme sind derzeit der Stand der Technik zur Überwachung von Pumpen, können jedoch nicht immer eindeutige Schlüsse auf die Ursachen von Schwingungen ziehen. In diesem Beitrag erfolgt ein Vergleich von druck- und schwingungsbasierten Zustandsüberwachungssystemen an einer typischen Kreiselpumpe.

2.2 Aufgabe¶

Die Aufgabe besteht darin, die übergebenen Daten aus dem Pumpenprüfstand im Labor der Hochschule Bochum auszuwerten und in ein geeignetes KI-Modell zu überführen. Das KI-Modell soll in der Lage sein, den Zustand der Pumpe zu erkennen und zu klassifizieren. Diese Erkennung soll anhand der Prognosefähigkeit des Modells die Wartungsintervalle und die Nutzungsdauer der Pumpe optimieren. Die Daten enthalten dabei sowohl Druck- als auch Schwingungssignale, die an unterschiedlichen Positionen an der Pumpe angebracht sind.

Positionierung Sensoren

Jede Messung wird dabei nach Erreichen eines stationären Förderstroms für 12 Minuten konstant gehalten. Während dieser Zeit werden die Messungen in hoher Frequenz aufgezeichnet und in den Formaten .mf4 und .csv abgespeichert. Beide Signalarten werden getrennt von einander durch zwei unterschiedliche System aufgezeichnet. Das KiDAQ-System ist für die Aufzeichnung der Drucksignale zuständig und das VIB-System ist für die Aufnahme der Schwingungssignale zuständig. Beide Signalarten sollen innerhalb des Projektes zur Erarbeitung eines KI-Modells beitragen. Ebenfalls soll bewertet werden, ob es vielleicht auch ausreichend ist nur eine Signalart für die Erkennung des Pumpenzustandes zu verwenden.

2.3 Daten¶

Wie bereits in der Vorstellung der Projektaufgabe kurz erläuert wurde, werden die Daten in zwei unterschiedlichen Formaten abgespeichert. Die Daten des KiDAQ-Systems werden im Format .mf4 abgespeichert und die Daten des VIB-Systems werden im Format .csv abgespeichert. Beide Datenarten werden im folgenden Abschnitt kurz erläutert.

2.3.1 KiDAQ-System Drucksignale:¶

Das KiDAQ-System zeichnet die Messwerte der Drucksignale mit 20 kHz auf (alle 0,00005 Sekunden ein Messwert). Hierfür werden piezoelektrische Sensoren verwendet, die an den Positionen P1 und P2 an der Pumpe angebracht sind. Die Sensoren sind in der Lage Druckänderungen zu messen und in elektrische Signale umzuwandeln. Um die Messungen zu homogenisieren existiert für jede Messsequenz ein Formblatt, welches genau dokumentiert welcher Förderstrom (l/min) vorliegt, an welchem Datum und zu welcher Zeit die Messung durchgeführt wurde und mit welcher Drehzahl die Pumpe angetrieben wurde. Ebenfalls wurde festgehalten, ob es sich bei dem Versuch, um einen Gut-, Kavitations-, Fehlausrichtungs- oder Laufradschadenversuch gehandelt hat. Da während der Messung eine große Datenanzahl generiert wird, wird das mf4-Format zum Abspeichern verwendet. Das mf4-Format ist ein standardisiertes Format, das in der Messtechnik und in der Datenanalyse eingesetzt wird.

2.3.2 VIB-System Schwingungssignale:¶

Das VIB-System ist für die Aufzeichnung der Schwingungssignals und zeichnet mit 30 kHz auf (Abstand der Messwerte beträgt 32600 ns). VIB steht hierbei für Vibration. Es werden auch hier spezielle Sensoren an der Pumpe angebracht. Insgesamt sind XX Sensoren verbaut, die alle ausgelesen werden. Die Schwingungssignale werden nach dem selben Prinzip wie die Drucksignale aufgezeichnet. Sie wandeln elektromagnetische Schwingungen in elektrische Signale um. Die Messungeinstellungen werden ebenfalls in einem Formblatt dokumentiert. Die Formblätter sind identisch zu den Formblättern des KiDAQ-Systems. Die Messungen werden in einem .csv-Format abgespeichert. Das .csv-Format lässt sich hierbei ohne Umformungsschritt sofort auslesen und vereinfacht somit die Arbeit, allerdings zu den Kosten einer größeren Einzeldatei.

2.4 Beschreibung der Rohdatenstruktur¶

Die Rohdaten des Pumpen Versuchs wurden ins in folgender Form übergeben:

  • Pumpensetup
    • Versuchsart
      • Formblatt
      • Aufzeichnungssystem
        • VIB-System
        • KiDAQ-System
        • Umrichterdaten

Das Pumpensetup gibt hierbei an, um welche Pumpe es sich handelt, und an welcher Stelle sich welche Sensoren befinden. Für unsere Auswertung wurden uns zwei unterschiedliche Setups übergeben. Das erste Setup nennt sich innerhalb der Ordnerstruktur Setup-I und das zweite Setup nennt sich Setup-II. In jedem Setup-Ordner befinden sich die Versuchsarten. Das Versuchsart-Verzeichnis gibt an, um welche Art von Versuch es sich handelt. Hierbei gibt es vier unterschiedliche Versuchsarten, welche in unterschiedlicher Anzahl vorliegen:

  • Gutversuch
  • Kavitationsversuch
  • Fehlausrichtungsversuch
  • Laufradschadenversuch

Alle Versuchsarten sind in den beiden Setups enthalten und werden im späteren Verlauf auch für die Erzeugung des KI-Modells und für die Validation eingesetzt.

Ebenfalls interessant ist das Formblatt. Es definiert in welcher Position sich die Pumpe zur Versuchsreihe befunden hat und zeigt ebenfalls die Ausrichtung der Messensensoren. Ebenfalls wird hier dokumentiert, welche Drehzahl die Pumpe angetrieben hat und welcher Förderstrom vorlag. Das Format der Formblätter ist für jede Versuchsart identisch.

Die beiden Aufzeichnungssysteme, die bereits im vorherigen Abschnitt erläutert wurden, enthalten die Messdaten: VIB für Schwingungssignale und KiDAQ für Drucksignale. Beide System verwenden unterschiedliche Dateiformate


Imports und globale Variablen¶

In [ ]:
from enum import Enum
import ipywidgets as widgets
from pandas import read_csv
import pandas as pd
import asammdf
from IPython.display import display
import os
import numpy as np
import time
import pathlib as pl
from IPython.display import clear_output
import pywt
from matplotlib import pyplot, ticker
In [ ]:
# Setting up the global variables

PATH_RAW_DATA = "E:/Messdaten/" #r"E:\Messdaten"
PATH_FEATURE_DATA = "./data/"
PATH_EXPLORATION_DATA = "./exploration/"
PATH_MODEL = "./models/"
DATA_SOURCE_KIDAQ = ["TEST_NAME", "TEST_TYPE", "RPM", "FLOW_RATE", "P1", "P2", "T1", "T2"]
RAW_DATA_TYPE = ["KIDAQ", "VIB"]

DATA_SOURCE_VID = [
    "TEST_NAME",
    "TEST_TYPE",
    "RPM",
    "FLOW_RATE",
    "S1",
    "S2",
    "S3",
    "S4",
    "S5",
    "S6",
    "S7",
    "S8",
]
FEATURE = [
    "STD",
    "RANGE",
    "IQR",
    "MEAN_MEDIAN",
    "FFT",
]
OPERATING_POINT_FREQ = [725, 1450, 2175, 2900]
OPERATING_POINT_FLOW_RATE = [0, 25, 50, 75, 100]

PREPROCESSING_WINDOW_SIZE_MS_DEFAULT = 1000
PREPROCESSING_WINDOW_SIZE_MS_MIN = 100
PREPROCESSING_WINDOW_SIZE_MS_MAX = 10000

PREPROCESSING_FREQUENCY_BAND_COUNT_DEFAULT = 10
PREPROCESSING_FREQUENCY_BAND_COUNT_MIN = 10
PREPROCESSING_FREQUENCY_BAND_COUNT_MAX = 100

DEFAULT_RAW_DATA_TYPE = RAW_DATA_TYPE[1]
DEFAULT_RAW_DATA = DATA_SOURCE_KIDAQ
DEFAULT_SAMPLE_RATE = 20000

DEFAULT_CLASS_LABEL = "TEST_TYPE"
# 2021-04-28 - Gut 1, 2021-05-19 - Kavitation 1, 2021-05-26 - Fehlausrichtung 1, 2021-07-10 - Laufradschaden 1
TEST_TYPES = ["Gut", "Kavitation", "Fehlausrichtung", "Laufradschaden"]
TEST_TYPE_REGEX = r"\b(?:" + "|".join(TEST_TYPES) + r")\b"

# ..\Messdaten\Setup-I\2021-04-28 - Gut 1\KiDAQ\1450rpm\1450rpm@100%.mf4
TEST_NAME_REGEX = r"\d{4}-\d{2}-\d{2} - [\w\s]+"

KIDAQ_FILE_SEARCH_PATTERN = "**/*.mf4"  # 725rpm@0%.mf4
KIDAQ_FILE_RPM_REGEX = r"(\d+)rpm"
KIDAQ_FILE_FLOW_RATE_REGEX = r"(\d+)%"


KIDAQ_DEFAULT_FEATURES = [
    "test_name",
    "test_type",
    "flow_rate",
    "rpm",
    "p1_std",
    "p2_std",
    "T1_std",
    "T2_std",
    "p1_range",
    "p2_range",
    "T1_range",
    "T2_range",
    "p1_iqr",
    "p2_iqr",
    "T1_iqr",
    "T2_iqr",
    "p1_mean_median",
    "p2_mean_median",
    "T1_mean_median",
    "T2_mean_median",
]

VIB_DEFAULT_FEATURES = [
    "test_name",
    "test_type",
    "flow_rate",
    "rpm",
    "s1_std",
    "s2_std",
    "s3_std",
    "s4_std",
    "s5_std",
    "s6_std",
    "s7_std",
    "s8_std",
    "s1_range",
    "s2_range",
    "s3_range",
    "s4_range",
    "s5_range",
    "s6_range", 
    "s7_range",
    "s8_range",
    "s1_iqr",
    "s2_iqr",
    "s3_iqr",
    "s4_iqr",
    "s5_iqr",
    "s6_iqr",
    "s7_iqr",
    "s8_iqr",
    "s1_mean_median",
    "s2_mean_median",
    "s3_mean_median",
    "s4_mean_median",
    "s5_mean_median",
    "s6_mean_median",
    "s7_mean_median",
    "s8_mean_median"
]
    
# 2900_25_1.mf4

KIDAQ_FILE_FIX_REGEX = r"(\d+)_(\d+)_(\d+).mf4"


VIB_FILE_SEARCH_PATTERN = (
    "**/VIB*.csv"  # 725rpm\0%\1\VIB1 2021-05-03 10-58-02.453.519.300.csv
)
VIB_FILE_SENSOR_REGEX = r"VIB(\d+)"
VIB_FILE_RPM_REGEX = r"(\d+)rpm"
VIB_FILE_FLOW_RATE_REGEX = r"(\d+)%"
VIB_SENSOR_COUNT = 8

DATANALYSIS_WINDOW_SIZE_DEFAULT = 100000
DATANALYSIS_WINDOW_SIZE_MIN = 0
DATANALYSIS_WINDOW_SIZE_MAX = 1000000

DATANALYSIS_STEP_SIZE_DEFAULT = 100000
DATANALYSIS_STEP_SIZE_MIN = 0
DATANALYSIS_STEP_SIZE_MAX = 1000000

3. Deskriptive und explorative Datenanalyse¶

3.1 Datenanalyse¶

Um die Daten deskriptiv und explorativ auszuwerten, müssen die Daten visualisert werden. Um diesen Prozess zu automatisieren und dadurch zu vereinfachen können innerhalb dieses interaktiven Abschnitts verschiedene Einstellungen vorgenommen werden, um sich Plots zu generieren. Diese Plots werden innerhalb des Notebooks angezeigt und können Einzeln oder als Serie abgespeichert werden.

3.2 Aggregative Analyse¶

In [ ]:
windowSizeSlider = widgets.IntSlider(
    value=DATANALYSIS_WINDOW_SIZE_DEFAULT,
    min=DATANALYSIS_WINDOW_SIZE_MIN,
    max=DATANALYSIS_WINDOW_SIZE_MAX,
    description='Window size:'
)
display(windowSizeSlider)

stepSizeSlider = widgets.IntSlider(
    value=DATANALYSIS_STEP_SIZE_DEFAULT,
    min=DATANALYSIS_STEP_SIZE_MIN,
    max=DATANALYSIS_STEP_SIZE_MAX,
    description='Step size:'
)
display(stepSizeSlider)


setups = list(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, x)), os.listdir(PATH_RAW_DATA)))
setupSelectionDropdown = widgets.Dropdown(
    options=setups,
    description='Setup:'
)
display(setupSelectionDropdown)


# list all folder in the selected setup and order by name of folder
seriesNames = sorted(list(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value, x)), os.listdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value)))))
seriesDropdown = widgets.Dropdown(
    options=seriesNames,
    description='Series:',
    value=seriesNames[1]
)
# if setupSelectionDropdown.value changes update seriesDropdown
def updateSeriesDropdown(change):
    seriesNames = sorted(list(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value, x)), os.listdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value)))))
    seriesDropdown.options = seriesNames
setupSelectionDropdown.observe(updateSeriesDropdown, names='value')
display(seriesDropdown)


dataSourceDropdown = widgets.Dropdown(
    options=RAW_DATA_TYPE,
    description='Data source:',
    value=RAW_DATA_TYPE[0]
)
display(dataSourceDropdown)


# create Pathlib Path to the selected series
def createPathToSeries(setup, series):
    return os.path.join(PATH_RAW_DATA, setup, series)

# list all files in all subfolders of createPathToSeries if data source is KIDAQ file should end with .mf4 else with .csv use function createPathToSeries
def listFilesInPath(path):
    files = []
    if dataSourceDropdown.value == 'KIDAQ':
        for file in pl.Path(path).glob(KIDAQ_FILE_SEARCH_PATTERN):
            # append full file path as string to files list
            files.append(file) 
    else:
        if path is None:
            return []
        for file in pl.Path(path).glob(VIB_FILE_SEARCH_PATTERN):
            files.append(file)
    return sorted(files)

fileDropdown = widgets.Dropdown(
    options=listFilesInPath(createPathToSeries(setupSelectionDropdown.value, seriesDropdown.value)),
    description='File:'
)
# if one of the dropdowns changes update fileDropdown
def updateFileDropdown(change):
    fileDropdown.options = listFilesInPath(createPathToSeries(setupSelectionDropdown.value, seriesDropdown.value))
setupSelectionDropdown.observe(updateFileDropdown, names='value')
seriesDropdown.observe(updateFileDropdown, names='value')
dataSourceDropdown.observe(updateFileDropdown, names='value')
display(fileDropdown)


aggregationDropdown = widgets.Dropdown(
    options=['median', 'std', 'min', 'max'],
    value='median',
    description='Aggregation:',
    disabled=False,
)
display(aggregationDropdown)

# add button with refresh icon to refresh columns of the selected file
refreshColumnsButton = widgets.Button(
    description='Refresh columns',
    disabled=False,
    button_style='success', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Refresh columns',
    icon='refresh'
)
display(refreshColumnsButton)

# read the selected file and return a pandas dataframe
def readFile():
    # check if path is not None return empty dataframe
    if fileDropdown.value is None:
        return pd.DataFrame()
    if dataSourceDropdown.value == 'KIDAQ':
        return asammdf.MDF(fileDropdown.value).to_dataframe()
    elif dataSourceDropdown.value == 'VIB':
        # return pd dataframe with only the first 2 columns delete the 3 row and set the second row as header
        df = pd.read_csv(fileDropdown.value, sep=';', encoding = "ISO-8859-1", header=1, usecols=[0,1], low_memory=False)
        # rename the first column to 'Timestamp [ns]'
        df.rename(columns={df.columns[0]: 'Timestamp [ns]'}, inplace=True)
        # delete the first row
        df = df.iloc[1:]
        # set the first column as index
        df.set_index(df.columns[0], inplace=True)
        # return the dataframe
        return df
        
        

# create a list of all columns of the selected file
def getColumnsToList():
    return list(readFile().columns.to_list())

multiselectColumns = widgets.SelectMultiple(
    options=[],
    description='Columns:',
    disabled=False
)
# if refreshColumnsButton is clicked update multiselectColumns
def refreshColumnsButtonClicked(b):
    multiselectColumns.options = getColumnsToList()
refreshColumnsButton.on_click(refreshColumnsButtonClicked)
# if one of the dropdowns changes update multiselectColumns to empty list
def updateMultiselectColumns(change):
    multiselectColumns.options = []
setupSelectionDropdown.observe(updateMultiselectColumns, names='value')
seriesDropdown.observe(updateMultiselectColumns, names='value')
dataSourceDropdown.observe(updateMultiselectColumns, names='value')
fileDropdown.observe(updateMultiselectColumns, names='value')
display(multiselectColumns)

def createPlot(columnTuple, aggregation, windowSize, stepSize, file):
    columnList = list(columnTuple)
    if len(columnList) == 0:
        return
    # if no columns are selected return
    if len(columnList) == 0:
        return
    df = readFile()
    # drop all columns that are not selected
    df = df[columnList]
    df_agg = df.rolling(
        window=windowSize,
        step=stepSize,
    ).agg(aggregation)
    # set title to the name of the selected file
    title=file.name
    # plot data
    df_agg.plot(title=title, grid=True)

# create button to plot the selected columns
plotButton = widgets.Button(
    description='Plot',
    disabled=True,
    button_style='success', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Plot',
    icon='check'
)
# if plotButton is clicked plot the selected columns
def plotButtonClicked(b):
    createPlot(multiselectColumns.value, aggregationDropdown.value, windowSizeSlider.value, stepSizeSlider.value, fileDropdown.value)
plotButton.on_click(plotButtonClicked)
# if no column is selected disable plotButton
def updatePlotButton(change):
    if len(multiselectColumns.value) == 0:
        plotButton.disabled = True
    else:
        plotButton.disabled = False
multiselectColumns.observe(updatePlotButton, names='value')
display(plotButton)
  • Über die Window-Size kann die Größer der Fenster eingestellt werden. Standardmäßig werden 100.000 Punkte zu einem Fenster zusammengefügt.
  • Über die Step-Size kann die Verschiebung der einzelnen Fenster eingestellt werden. Stellen wir diesen ebenfalls auf 100.000 kommt es zu keiner Überlappung, da jedes Fenster immer um 100.000 Punkte weitergeschoben wird. Stellen wir allerdings die Step-Size kleiner als die Window-Size, so kommt es zu einer Überlappung der Fenster, da die selben Punkte nun in mehreren Fenstern liegen können.
  • Das Setup kann hierbei ausgewählt werden. Es stehen die beiden Setups Setup-I und Setup-II zur Verfügung.
  • Die Series gibt an, welche Versuchsreihe ausgewählt werden sollen. Es können hierbei innerhalb des ausgewählten Setups auf die einzelnen Versuchsreihen zugegriffen werden.
  • Data Source ist für die jeweilige Aufzeichnungsart verantwortlich. Es kann zwischen KIDAQ und VIB gewählt werden.
  • File ist für die Auswahl der Datei verantwortlich, die ausgewertet und geplottet werden soll.
  • Die letzte Einstellung beinhaltet die Aggregation der Werte. Es kann hierbei zwischen 'median', 'std', 'min' und 'max' gewählt werden. Die Aggregation bezieht sich hierbei immer auf die einzelnen Fenster. Alle Punkte innerhalb des Fenstern werden mittels der Aggregation zusammen gerechnet und es entsteht ein neuer einzelner Punkte pro Fenster. Aus diesem Grund ist es manchmal Ratsam eine Fensterüberlappung zu zulassen.
  • Columns gibt an, welche Spalten ausgewählt werden sollen. Hierbei können mehrere Spalten ausgewählt werden. Die Spalten werden dann mit unterschiedlichen Farben geplottet.

3.3 Frequentative Analyse¶

3.3.1 Fast-Fourier-Transformation (FFT)¶

Die Fast-Fourier-Transformation ist ein effizienter Algorithmus zur Berechnung der Diskreten-Fourier-Transformation (DFT) einer Sequenz von Datenpunkten. Die DFT wandelt eine zeitliche Darstellung eines Signals in den Frequenzbereich um und ermöglicht so die Analyse der Frequenzkomponenten eines Signalraums. Die Fast-Fourier-Transformation wurde entwickelt, um die Berechnungszeit der DFT erheblich zu reduzieren, insbesondere bei großen Mengen von Daten, wie wir sie aus dem Bereich von Big Data kennen.

Wie funktioniert die Fast-Fourier-Transformation?

  1. Die FFT teilt das Eingangssignal in Teilmengen auf, wodurch es rekursiv in kleinere Teilprobleme aufgeteilt wird.
  2. Durch die Ausnutzung symmetrischer und periodischer Eigenschaften der DFT können viele Berechnungen wiederverwendet werden, was zu einer drastischen Reduzierung der Rechenzeit führt.
  3. Die Berechnungen erfolgen in einem Schritt-für-Schritt-Verfahren, bei dem die Ergebnisse der Teilmengen kombiniert werden, um das endgültige Frequenzspektrum zu erhalten.

Wann wird die Kurzzeit-Fourier-Transformation angewendet? Die FFT wird vor allem für die Analyse von Signalen verwendet, wenn es notwendig wird, das Frequenzspektrum zu analysieren oder zu visualisieren.

  1. Frequenzinhalt: Die FFT zeigt, welche Frequenzen im Vibrationssignal vorhanden sind und in welchem Maße sie vertreten sind. Dies ermöglicht die Identifizierung von dominierenden Frequenzen oder Frequenzbereichen.
  2. Amplitudenverteilung: Die Amplituden der Frequenzkomponenten im Vibrationssignal werden dargestellt. Dadurch kann man herausfinden, wie stark die einzelnen Frequenzen in Bezug auf ihre Amplitude sind. Dies hilft bei der Analyse der Energieverteilung im Frequenzbereich.
  3. Peak-Erkennung: Die FFT kann helfen, Peaks oder Spitzen im Frequenzspektrum zu identifizieren. Diese Peaks können auf resonante oder periodische Eigenschaften des Systems hinweisen und Informationen über potenzielle Fehler oder Probleme liefern.
  4. Harmonische Analyse: Durch die FFT können harmonische Komponenten identifiziert werden, die durch periodische Schwingungen im Vibrationssignal entstehen. Dies kann auf Unregelmäßigkeiten im System oder auf das Vorhandensein von störenden Frequenzen hinweisen.
  5. Filterung: Durch die Analyse des Frequenzspektrums mit Hilfe der FFT kann man unerwünschte Frequenzkomponenten identifizieren und gezielt herausfiltern. Dies ermöglicht es, spezifische Frequenzen zu isolieren oder Störungen zu reduzieren.
  6. Zeit-Frequenz-Analyse: Durch die Anwendung von Fensterfunktionen auf das Vibrationssignal vor der FFT kann eine Zeit-Frequenz-Analyse durchgeführt werden. Diese Technik ermöglicht die Beobachtung von Frequenzänderungen im Laufe der Zeit und kann bei der Diagnose von instationären oder sich ändernden Zuständen helfen.

Die FFT ist ein leistungsfähiges Werkzeug zur Analyse von Vibrationssignalen und findet in verschiedenen Anwendungen wie der Zustandsüberwachung, der Schadenserkennung, der Maschinendiagnose und der akustischen Analyse Anwendung.

3.3.2 Kurzzeit-Fourier-Transformation (STFT)¶

Die Kurzzeit-Fourier-Transformation (englisch: Short-Time Fourier Transformation, STFT) ist ein Verfahren zur Analyse von zeitabhängigen Signalen. Es sind handelt sich dabei um eine Erweiterung der klassischen Fourier-Transformation, die auf stationäre Signale angewendet wird und diese in ihre Frequenzkomponenten zerlegt. Im Gegensatz dazu erlaubt die STFT die Analyse von nicht-stationären Signalen, bei denen sich die Frequenzinhalte im Laufe der Zeit ändern können.

Wie funktioniert die Kurzzeit-Fourier-Transformation?

  1. Das Eingangssignal wird in kleine, überlappende Segmente (Fenster) aufgeteilt.
  2. Jedes Fenster wird mit einer Fensterfunktion multipliziert, um sogenannte Leakage-Effekte zu minimieren. Typische Fensterfunktionen sind z. B. das Hamming-Fenster oder das Hann-Fenster.
  3. Für jedes Fenster wird dann die klassische Fourier-Transformation angewendet, um es in den Frequenzbereich zu überführen.
  4. Die resultierenden Spektren der einzelnen Fenster werden in einem sogenannten Zeit-Frequenz-Diagramm dargestellt.

Wann wird die Kurzzeit-Fourier-Transformation angewendet? Die STFT wird häufig zur Analyse von Audiosignalen verwendet, da diese in der Regel nicht-stationär sind. Sie wird z. B. zur Spracherkennung, zur Musikanalyse oder zur Analyse von Umgebungsgeräuschen eingesetzt. Genau dieser Askpekt macht die STFT auch für die Analyse von Vibrationssignalen interessant, da diese in der Regel ebenfalls nicht-stationär sind und sich die Frequenzinhalte im Laufe der Zeit ändern können.

Die STFT ist ein vielseitiges Werkzeug in der Datenanalyse von Vibrationssignalen. Sie ermöglicht es, das Frequenzverhalten von Signalen über die Zeit zu verfolgen und so ein umfassendes Verständnis ihrer Dynamik zu erlangen.

3.3.3 Wavelet-Transform¶

Die Wavelet-Transformation ist eine mathematische Methode zur Analyse von Signalen, die eine alternative Darstellung des Signals im Zeit-Frequenz-Bereich ermöglicht. Im Gegensatz zur Fourier-Transformation und der Fast-Fourier-Transformation, die das Signal nur in den Frequenzbereich umwandeln, liefert die Wavelet-Transformation auch zeitliche Informationen über die Frequenzanteile eines Signals. Dadurch ist sie besonders gut geeignet, um nicht-stationäre Signale mit zeitlich variierenden Frequenzinhalten zu analysieren. Ein entscheidener Vorteil der Wavelet-Transformation zu den bereits oben vorgestellten Methoden liegt in der Skalierung der Fenster. Dadurch können höhere Frequenzen mit einer besseren zeitlichen Auflösung analysiert werden.

Wie funktioniert die Wavelet-Transformation?

  • Die Wavelet-Transformation verwendet spezielle Basisfunktionen, die als Wavelets bezeichnet werden. Diese Wavelets werden nach ihren Eigenschaft ausgewählt: Sie ermöglichen zeitliche und frequenzmäßige Lokalisierung
  • Ähnlich wie bei der Fourier-Transformation wird das Eingangssignal in den Frequenzbereich transformiert. Allerdings wird die Transformation hier nicht nur einmal angewendet, sondern mehrfach mit unterschiedlichen Wavelets und Skalierungen. Dadurch wird das Signal in unterschiedliche Frequenzbereiche zerlegt, die jeweils eine andere zeitliche Auflösung haben. Die Wavelet-Transformation liefert also eine Darstellung des Signals im Zeit-Frequenz-Bereich.
  • Die Wavelet-Transformation ist eine skalierbare Analysemethode, bei der man verschiedene Auflösungsstufen (Skalierungen) verwenden kann, um feinere oder grobere Zeit-Frequenz-Darstellungen zu erhalten

Wann wird die Wavelet-Transformation angewendet?

  • Wenn man an zeitlicher und frequenzmäßiger Lokalisierung von Signalen interessiert ist
  • Bei der Analyse von nicht-stationären Signalen mit sich ändernden Frequenzinhalten
  • Wenn man spezielle Merkmale in einem Signal hervorheben oder Rauschen reduzieren möchte
  • Wenn man detaillierte Informationen über die zeitliche Struktur eines Signals benötigt

Die Wavelet-Transformation ist in der Lage zeitlichen und frequenzmäßigen Lokalisierung zu ermöglichen und macht sie besonders geeignet für die Analyse von komplexen und nicht-stationären Signalen, an welchen die normalen Methoden der Fourier-Transformation scheitern.

In [ ]:
# FFT Analysis configuration
setups = list(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, x)), os.listdir(PATH_RAW_DATA)))
setupSelectionDropdown = widgets.Dropdown(
    options=setups,
    description='Setup:'
)
display(setupSelectionDropdown)


# list all folder in the selected setup and order by name of folder
seriesNames = sorted(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value, x)), os.listdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value))))
seriesDropdown = widgets.Dropdown(
    options=seriesNames,
    description='Series:',
    value=seriesNames[1]
)
# if setupSelectionDropdown.value changes update seriesDropdown
def updateSeriesDropdown(change):
    seriesNames = sorted(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value, x)), os.listdir(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value))))
    seriesDropdown.options = seriesNames
setupSelectionDropdown.observe(updateSeriesDropdown, names='value')
display(seriesDropdown)


dataSourceDropdown = widgets.Dropdown(
    options=RAW_DATA_TYPE,
    description='Data source:',
    value=RAW_DATA_TYPE[0]
)
display(dataSourceDropdown)

fileDropdown = widgets.Dropdown(
    options=[],
    description='File:'
)
display(fileDropdown)


def updateFileDropdown(change):
    global fileDropdown, setupSelectionDropdown, seriesDropdown, dataSourceDropdown
    fileDropdown.options = pl.Path(os.path.join(PATH_RAW_DATA, setupSelectionDropdown.value, seriesDropdown.value)).glob(KIDAQ_FILE_SEARCH_PATTERN if dataSourceDropdown.value == 'KIDAQ' else VIB_FILE_SEARCH_PATTERN)

setupSelectionDropdown.observe(updateFileDropdown, names='value')
seriesDropdown.observe(updateFileDropdown, names='value')
dataSourceDropdown.observe(updateFileDropdown, names='value')
updateFileDropdown(None)


multiSelectColumns = widgets.SelectMultiple(
    options=[],
    description='Columns:',
    disabled=False
)
loadDataButton = widgets.Button(
    description='Load data',
    disabled=False,
    tooltip='Load data',
    icon='refresh'
)
fftAnalysisData = None

display(loadDataButton)
display(multiSelectColumns)


sampleRateInput = widgets.IntText(
    value=DEFAULT_SAMPLE_RATE,
    description='Sample rate:',
)
display(sampleRateInput)

maxFrequencyInput = widgets.IntText(
    value=DEFAULT_SAMPLE_RATE / 10,
    description='Max. frequency:',
)
display(maxFrequencyInput)


analyseTypeDropdown = widgets.Dropdown(
    options=['FFT', 'STFT', 'Wavelet','FFT - Time'],
    description='Analyse type:',
    value='FFT',
)

display(analyseTypeDropdown)

waveletDropdown = widgets.Dropdown(
    options=pywt.wavelist(kind='continuous'),
    description='Wavelet:',
    value='mexh',
)

display(waveletDropdown)

windowSizeSlider = widgets.IntSlider(
    value=PREPROCESSING_WINDOW_SIZE_MS_DEFAULT,
    min=PREPROCESSING_WINDOW_SIZE_MS_MIN,
    max=50000,
    step=100,
    description='Window size (ms):'
)

display(windowSizeSlider)

windowIndexOffsetSlider = widgets.IntSlider(
    value=0,
    min=0,
    max=100,
    description='index offset:',
    disabled=True
)

display(windowIndexOffsetSlider)

fftStepSizeSlider = widgets.IntSlider(
    value=0,
    min=100,
    max=1000,
    step=50,
    description='stepsize:',
)

display(fftStepSizeSlider)


def analyseTypeChanged(change):
    global fftStepSizeSlider
    fftStepSizeSlider.disabled = change == None or change.new != 'FFT - Time'
    waveletDropdown.disabled = change == None or change.new != 'Wavelet'

analyseTypeChanged(None)

analyseTypeDropdown.observe(analyseTypeChanged, names='value')

def updateIndexOffsetSlider(change):
    global windowIndexOffsetSlider, windowSizeSlider, fftAnalysisData
    if fftAnalysisData is None:
        windowIndexOffsetSlider.disabled = True
        return
    windowIndexOffsetSlider.disabled = False
    windowIndexOffsetSlider.max = len(fftAnalysisData) // (sampleRateInput.value * (windowSizeSlider.value // 1000)) - 1

def loadData(change):
    global multiSelectColumns, fileDropdown, fftAnalysisData, loadDataButton, dataSourceDropdown

    fftAnalysisData = None
    loadDataButton.button_style = 'warning'
    loadDataButton.description = 'Loading...'
    match dataSourceDropdown.value:
        case 'KIDAQ':
            fftAnalysisData = asammdf.MDF(fileDropdown.value).to_dataframe()
            fftAnalysisData.index = pd.to_timedelta(fftAnalysisData.index, unit="s")
        case 'VIB':
            fftAnalysisData = pd.read_csv(fileDropdown.value, skiprows=2, encoding="ISO-8859-1", sep=";", index_col=0)
            fftAnalysisData.index = pd.to_datetime(fftAnalysisData.index, unit="ns")

    multiSelectColumns.options = fftAnalysisData.columns.to_list() if fftAnalysisData is not None else []
    loadDataButton.button_style = 'success'
    loadDataButton.description = 'Load data'

    windowSizeSlider.max = len(fftAnalysisData) // sampleRateInput.value * 1000
    updateIndexOffsetSlider(None)


loadDataButton.on_click(loadData)
sampleRateInput.observe(updateIndexOffsetSlider, names='value')
windowSizeSlider.observe(updateIndexOffsetSlider, names='value')
In [ ]:
# RUN FFT ANALYSIS

import scipy 
dataframe = fftAnalysisData[list(multiSelectColumns.value)]
dataframe = dataframe.resample(f"{windowSizeSlider.value}ms")

# Get the first window
dataframe = dataframe.get_group(
    list(dataframe.groups.keys())[windowIndexOffsetSlider.value]
)

for i, column in enumerate(dataframe.columns):
    match analyseTypeDropdown.value:
        case "FFT":
            # Erstellung der Plots für jede Spalte
            # Extrahieren der Vibrationsdaten aus der aktuellen Spalte
            values = dataframe[column].values

            # Durchführung des FFT
            fft = np.fft.fft(values)
            freqFreq = np.fft.fftfreq(len(values), 1 / sampleRateInput.value)

            fftDf = pd.DataFrame(np.abs(fft), index=freqFreq)
            fftDf = fftDf.loc[
                (fftDf.index > 0) & (fftDf.index <= maxFrequencyInput.value)
            ]
            
            # Plotten des Amplitudenspektrums and tight_layout()
            fftDf.plot(
                xlabel="Frequency",
                ylabel="Amplitude",
                grid=True,
                legend=False,
                title=f"FFT Plot - {column}",
            )
        case "STFT":
            # Erstellung der Plots für jede Spalte
            # Extrahieren der Vibrationsdaten aus der aktuellen Spalte
            values = dataframe[column].values

            # Durchführung des FFT
            f, t, Zxx = scipy.signal.stft(values, sampleRateInput.value, nperseg=1000 * ((sampleRateInput.value // 4) // maxFrequencyInput.value))
            
            fMaxIndex = np.argmin(np.abs(f - maxFrequencyInput.value))
            f = f[:fMaxIndex]
            Zxx = Zxx[:fMaxIndex]

            pyplot.pcolormesh(t, f, np.abs(Zxx), shading='gouraud')
            pyplot.title(f'STFT Magnitude für {column}')
            pyplot.ylabel('Frequency [Hz]')
            pyplot.xlabel('Time [sec]')
            pyplot.show()
            
        case "Wavelet":
            coefficients, frequencies = pywt.cwt(
                dataframe[column].values,
                np.arange(1, maxFrequencyInput.value),
                waveletDropdown.value,
                sampling_period=1 / sampleRateInput.value,
                method="fft",
            )

            coefficients = coefficients[::-1, :]
            frequencies = frequencies[::-1]

            fig, ay = pyplot.subplots(figsize=(12, 6))
            pyplot.imshow(
                np.abs(coefficients),
                aspect="auto",
                extent=[0, windowSizeSlider.value, 1, maxFrequencyInput.value],
                cmap="jet",
            )
            pyplot.colorbar(label="Amplitude")
            pyplot.title(f"CWT: Zeit-Frequenz-Analyse für {column}")
            pyplot.xlabel("Zeit (ms)")
            pyplot.ylabel("Frequenz")
            pyplot.yscale("log")
            ay.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.0f"))
            pyplot.show()
        case "FFT - Time":
            dataframe = dataframe.resample(f"{fftStepSizeSlider.value}ms")
            coefficients = []
            frequencies = None
            for window in dataframe:
                values = window[1][column].values
                frequencies = np.fft.fftfreq(len(values), 1 / sampleRateInput.value)

                fft = np.fft.fft(values)

                fftDf = pd.DataFrame(np.abs(fft), index=frequencies)
                fftDf = fftDf.loc[
                    (fftDf.index > 0) & (fftDf.index <= maxFrequencyInput.value)
                ]
                coefficients.append(fftDf.values.flatten())

            maxLength = max(arr.shape[0] for arr in coefficients)
            coefficients = [
                np.pad(arr, (0, maxLength - arr.shape[0])) for arr in coefficients
            ]

            coefMatrix = np.vstack(coefficients)[:, ::-1].T

            pyplot.figure(figsize=(12, 6))
            pyplot.imshow(
                coefMatrix,
                aspect="auto",
                extent=[0, windowSizeSlider.value, 1, maxFrequencyInput.value],
                cmap="jet",
            )
            pyplot.colorbar(label="Amplitude")
            pyplot.title(f"FFT: Zeit-Frequenz-Analyse für {column} (Zeitfenster: {fftStepSizeSlider.value}ms)")
            pyplot.xlabel("Zeit (ms)")
            pyplot.ylabel("Frequenz")
            pyplot.show()
  • Das Setup kann hierbei ausgewählt werden. Es stehen die beiden Setups Setup-I und Setup-II zur Verfügung.
  • Die Series gibt an, welche Versuchsreihe ausgewählt werden sollen. Es können hierbei innerhalb des ausgewählten Setups auf die einzelnen Versuchsreihen zugegriffen werden.
  • Data Source ist für die jeweilige Aufzeichnungsart verantwortlich. Es kann zwischen KIDAQ und VIB gewählt werden.
  • File ist für die Auswahl der Datei verantwortlich, die ausgewertet und geplottet werden soll.
  • Columns gibt an, welche Spalten ausgewählt werden sollen. Hierbei können mehrere Spalten ausgewählt werden. Die Spalten werden dann mit unterschiedlichen Farben geplottet.

  • Die Samplerate gibt an, wie viele Punkte pro Sekunde aufgezeichnet wurden. Diese Information ist wichtig, um die Frequenzanalyse durchführen zu können.

  • Maximale Frequency gibt an, bis zu welcher Frequenz die Frequenzanalyse durchgeführt werden soll.
  • Der Analyse-Typ kann über ein Dropdown-Menü eingestellt werden. Es gibt als Auswahl: FFT, SFTF, Wavelet und FFT - Time
  • Sollte Wavelet als Analyse-Typ gewählt werden, kann zusätzlich noch eine Wavelet-Funktion ausgewählt werden, welche Einfluss auf die Berechnung des Wavelets hat.
  • Window-Size gibt an, wie viele Punkte für die Analyse pro Fenster verwendet werden sollen.
  • Mit dem Index-Offset Slider kann nun das zu betrachtende Window ausgewählt werden. Es orientiert sich an der Anzahl der resultierenden Windows, welche mittels der Window-Size eingestellt wurden.

4. Preprocessing¶

Aufgeteilt nach KIDAQ und VIB

  • Auswahl der Fenstergröße in Millisekunden
  • Auswahl des Setups
  • Frequenzanalyse (Fourier-Transformation)
    • Abtastrate
    • Maximalfrequenz
    • Feature Resampling
    • Fenstergröße der Aggregationen nach Frequenzbereichen

4.1 Parametrierung¶

In [ ]:
# Preprocessing parameters

windowSizeSlider = widgets.IntSlider(
    value=PREPROCESSING_WINDOW_SIZE_MS_DEFAULT,
    min=PREPROCESSING_WINDOW_SIZE_MS_MIN,
    max=PREPROCESSING_WINDOW_SIZE_MS_MAX,
    step=100,
    description='Window size:'
)
display(windowSizeSlider)

dataSourceDropdown = widgets.Dropdown(
    options=RAW_DATA_TYPE,
    description='Data source:'
)

display(dataSourceDropdown)

setups = list(filter(lambda x: os.path.isdir(os.path.join(PATH_RAW_DATA, x)), os.listdir(PATH_RAW_DATA)))

setupSelectionDropdown = widgets.Dropdown(
    options=setups,
    description='Setup:'
)

display(setupSelectionDropdown)



sampleRateInput = widgets.IntText(
    value=DEFAULT_SAMPLE_RATE,
    description='Sample rate:',
)



maxFrequencyInput = widgets.IntText(
    value=DEFAULT_SAMPLE_RATE / 10,
    description='Max. frequency:',
)

frequencyResampleInput = widgets.IntText(
    value=DEFAULT_SAMPLE_RATE / 100,
    description='Resample count:',
)

frequencyBandCountSlider = widgets.IntSlider(
    value=PREPROCESSING_FREQUENCY_BAND_COUNT_DEFAULT,
    min=PREPROCESSING_FREQUENCY_BAND_COUNT_MIN,
    max=PREPROCESSING_FREQUENCY_BAND_COUNT_MAX,
    step=10,
    description='Freq. bands:'
)

displayed = False
def showFrequencyAnalysisInput():
    global displayed
    if not displayed:
        display(sampleRateInput)
        display(maxFrequencyInput)
        display(frequencyResampleInput)
        display(frequencyBandCountSlider)
        displayed = True

selectFrequencyAnalysis = widgets.Checkbox(
    value=False,
    description='Frequency analysis:'
)

selectFrequencyAnalysis.observe(lambda x: showFrequencyAnalysisInput(), names='value')

display(selectFrequencyAnalysis)
    
In [ ]:
# FFT helper functions

from matplotlib import pyplot
import numpy as np
import pandas as pd
from scipy import stats
from scipy.stats import skew, kurtosis
from scipy.signal import find_peaks


def compute_top_n_amplitudes(fft_values, n):
    sorted_indices = np.argsort(fft_values)  # Sortiere die Amplituden
    top_indices = sorted_indices[-n:]  # Wähle die Top-N-Amplituden aus
    top_amplitudes = fft_values[top_indices]
    return top_amplitudes[::-1]


def compute_top_n_frequencies(fft_values, n):
    sorted_indices = np.argsort(fft_values)  # Sortiere die Amplituden
    top_frequencies = sorted_indices[-n:]  # Wähle die Top-N-Amplituden aus
    return top_frequencies[::-1]


def compute_energy_in_bands(fft_values, bands):
    energy = []
    for band in bands:
        band_values = fft_values[band[0] : band[1]]  # Frequenzbereich für das Band
        band_energy = np.sum(np.abs(band_values) ** 2)  # Berechne die Energie im Band
        energy.append(band_energy)
    return energy


def compute_spectral_features(fft_values):
    spectral_skewness = skew(np.abs(fft_values))
    spectral_kurt = kurtosis(np.abs(fft_values))
    amplitudes = np.abs(fft_values)  # Berechnung der Amplituden
    geometric_mean = np.exp(
        np.mean(np.log(amplitudes[amplitudes != 0]))
    )  # Berechnung des geometrischen Mittelwerts

    spectral_flatness = geometric_mean / (
        np.mean(amplitudes) + np.finfo(float).eps
    )  # Berechnung des spektralen Flachheitsmaßes
    return spectral_skewness, spectral_kurt, spectral_flatness


def compute_frequency_bands_mean(fft_values, bands):
    band_features = []
    for band in bands:
        band_values = fft_values[band[0] : band[1]]
        band_feature = np.mean(
            np.abs(band_values)
        )  # Hier: Durchschnittliche Amplitude im Band
        band_features.append(band_feature)
    return band_features


def compute_harmonic_dominance(fft_values):
    fundamental_amplitude = np.abs(fft_values[0])  # Amplitude der Grundfrequenz
    harmonic_amplitudes = np.abs(
        fft_values[1:]
    )  # Amplituden der harmonischen Frequenzen
    total_harmonic_amplitude = np.sum(
        harmonic_amplitudes
    )  # Summe der harmonischen Amplituden
    harmonic_dominance = total_harmonic_amplitude / (
        fundamental_amplitude + total_harmonic_amplitude
    )
    return harmonic_dominance


def compute_periodicity(fft_values, n):
    fundamental_amplitude = np.abs(fft_values[0])  # Amplitude der Grundfrequenz
    residual_amplitudes = np.abs(
        fft_values[1:]
    )  # Amplituden der nicht-grundfrequenten Komponenten
    significant_residuals = residual_amplitudes[-n:]  # Filtere signifikante Komponenten
    periodicity = np.sum(significant_residuals) / fundamental_amplitude
    return periodicity


def count_harmonic_peaks(fft_values, relative_peak_threshold):
    threshold = relative_peak_threshold * np.max(
        np.abs(fft_values)
    )  # Schwellwert für die Peak-Erkennung
    peaks, _ = find_peaks(
        np.abs(fft_values), height=threshold
    )  # Finde Peaks im Amplitudenspektrum
    harmonic_peak_count = (
        len(peaks) - 1
    )  # Zähle die Anzahl der Peaks (ohne die Grundfrequenz)
    return harmonic_peak_count


def extractBandFeatures(
    fft,
    fftFreq,
    maxFrequency,
    resolution,
    frequencyBandCount,
    relativePeakThreshold=0.25,
):
    fftDf = pd.DataFrame(np.abs(fft), index=fftFreq)
    fftDf = fftDf.loc[(fftDf.index >= 0) & (fftDf.index <= maxFrequency)]

    flattenedFFT = fftDf.values.flatten()
    bands = [
        [
            int(i * maxFrequency * resolution / frequencyBandCount),
            (int((i + 1) * maxFrequency * resolution / frequencyBandCount) - 1),
        ]
        for i in range(frequencyBandCount)
    ]
    top_n_amplitudes = compute_top_n_amplitudes(flattenedFFT, frequencyBandCount)
    top_n_frequencies = compute_top_n_frequencies(flattenedFFT, frequencyBandCount)
    band_energy = compute_energy_in_bands(flattenedFFT, bands)
    spectral_skewness, spectral_kurt, spectral_flatness = compute_spectral_features(
        flattenedFFT
    )
    frequency_bands = compute_frequency_bands_mean(flattenedFFT, bands)
    harmonic_dominance = compute_harmonic_dominance(flattenedFFT)
    periodicity = compute_periodicity(flattenedFFT, frequencyBandCount)
    harmonic_peak_count = count_harmonic_peaks(flattenedFFT, relativePeakThreshold)
    data = np.concatenate(
        (
            top_n_amplitudes,
            top_n_frequencies / resolution,
            band_energy,
            frequency_bands,
            [
                spectral_skewness,
                spectral_kurt,
                spectral_flatness,
                harmonic_dominance,
                periodicity,
                harmonic_peak_count,
            ],
        )
    )

    feature_names = np.concatenate(
        (
            [f"amplitude_{i+1}_highest_energy" for i in range(frequencyBandCount)],
            [f"amplitude_{i+1}_highest_freq" for i in range(frequencyBandCount)],
            [
                f"band_{band[0]/resolution}_{band[1]/resolution}_energy"
                for band in bands
            ],
            [
                f"band_{band[0]/resolution}_{band[1]/resolution}_energy_mean"
                for band in bands
            ],
            [
                "spectral_skewness",
                "spectral_kurt",
                "spectral_flatness",
                "harmonic_dominance",
                "periodicity",
                "harmonic_peak_count",
            ],
        )
    )

    df = pd.DataFrame(data)
    df.index = feature_names

    return df


def extractFlattenedFeatureNames(dataFrame):
    featureNames = []
    for row in dataFrame.index:
        for col in dataFrame.columns:
            featureNames.append("_".join([row, col]))
    return featureNames


def extractAggregationFeatures(fft, fftFreq, maxFrequency, aggregationResampleCount):
    fftDf = pd.DataFrame(np.abs(fft), index=fftFreq)
    fftDf = fftDf.loc[(fftDf.index >= 0) & (fftDf.index <= maxFrequency)]

    windowSize = int(maxFrequency / aggregationResampleCount)
    fftMax = fftDf.max()

    windowLabels = [
        f"{windowSize * i}_{windowSize * (i+1)}"
        for i in range(aggregationResampleCount)
    ]
    fftDf.index = pd.cut(
        fftDf.index, bins=aggregationResampleCount, labels=windowLabels
    )
    fftWindows = fftDf.groupby(fftDf.index)
    fftDf = pd.concat(
        [
            fftWindows.std().fillna(0),
            (fftWindows.max() - fftWindows.min()).fillna(0),
            fftWindows.aggregate(lambda sample: stats.iqr(sample.to_numpy())).fillna(0),
            (fftWindows.mean() - fftWindows.median()).fillna(0),
            (fftWindows.max() / fftMax).fillna(0),
        ],
        axis=1,
    )
    fftDf.columns = ["std", "range", "iqr", "mean_median", "relative_max"]
    fftDf.index.name = "freq_window"

    return pd.DataFrame(
        fftDf.to_numpy().flatten(), index=extractFlattenedFeatureNames(fftDf)
    )


def fftFeatureExtraction(
    samples, sampleRate, maxFrequency, aggregationResampleCount, frequencyBandCount
):
    resolution = len(samples) / sampleRate
    fft = np.fft.fft(samples)
    fftFreq = np.fft.fftfreq(len(samples), 1 / sampleRate)
    aggregationFeatures = extractAggregationFeatures(
        fft, fftFreq, maxFrequency, aggregationResampleCount
    )
    bandFeatures = extractBandFeatures(
        fft, fftFreq, maxFrequency, resolution, frequencyBandCount
    )
    return pd.concat([aggregationFeatures, bandFeatures])

4.2 Start¶

In [ ]:
# Start the preprocessing
import re
from asammdf import MDF
from tqdm.notebook import tqdm
from matplotlib import pyplot


windowSize = windowSizeSlider.value
setup = setupSelectionDropdown.value
rawDataType = dataSourceDropdown.value
frequencyAnalysis = selectFrequencyAnalysis.value
sampleRate = sampleRateInput.value
maxFrequency = maxFrequencyInput.value
resampleCount = frequencyResampleInput.value
frequencyBandCount = frequencyBandCountSlider.value

fileName = f"{setup}_{rawDataType}_{windowSize}.csv"

if frequencyAnalysis:
    fileName = f"{setup}_{rawDataType}_{windowSize}ws_{sampleRate}sr_{maxFrequency}mf_{resampleCount}rc_{frequencyBandCount}fbc.csv"


outputFile = open(
    os.path.join(PATH_FEATURE_DATA, fileName),
    "w",
)

headerWritten = False
match rawDataType:
    case "KIDAQ":
        columns = KIDAQ_DEFAULT_FEATURES
        sensorInColumns = []

        kidaqFiles = list(
            pl.Path(os.path.join(PATH_RAW_DATA, setup)).glob(KIDAQ_FILE_SEARCH_PATTERN)
        )
        for file in tqdm(kidaqFiles):
            testName = re.search(TEST_NAME_REGEX, str(file)).group(0)
            testType = re.search(TEST_TYPE_REGEX, str(file)).group(0)
            secondFormat = re.search(KIDAQ_FILE_FIX_REGEX, file.name)
            if secondFormat:
                rpm = secondFormat.group(1)
                flowRate = secondFormat.group(2)
            else:
                rpm = re.search(KIDAQ_FILE_RPM_REGEX, file.name).group(1)
                flowRate = re.search(KIDAQ_FILE_FLOW_RATE_REGEX, file.name).group(1)

            mdf = MDF(file)
            df = mdf.filter(["p1", "p2", "T1", "T2"]).to_dataframe()

            df.index = pd.to_timedelta(df.index, unit="s")
            resampled = df.resample(str(windowSize) + "ms")

            median = resampled.median().to_numpy()
            mean = resampled.mean().to_numpy()
            max_val = resampled.max().to_numpy()
            min_val = resampled.min().to_numpy()
            std = resampled.std().to_numpy()
            range_val = max_val - min_val
            iqr = resampled.aggregate(
                lambda sample: stats.iqr(sample.to_numpy())
            ).to_numpy()

            result = np.concatenate(
                (
                    np.repeat(
                        [[testName, testType, flowRate, rpm]],
                        len(std),
                        axis=0,
                    ),
                    std,
                    range_val,
                    iqr,
                    median - mean,
                ),
                axis=1,
            )
            if frequencyAnalysis:
                fftAnalysisWindows = []
                for samples in resampled:
                    fftAnalysis = []
                    for sensor in samples[1]:
                        frequencyFeatures = fftFeatureExtraction(
                            samples[1][sensor],
                            sampleRate,
                            maxFrequency,
                            resampleCount,
                            frequencyBandCount,
                        )
                        if not sensor in sensorInColumns:
                            columns = np.concatenate(
                                (columns, sensor + "_" + frequencyFeatures.index)
                            )
                            sensorInColumns.append(sensor)
                        fftAnalysis.append(frequencyFeatures.to_numpy().flatten())
                    fftAnalysisWindows.append(np.concatenate(fftAnalysis, axis=0))
                result = np.concatenate(
                    (result, fftAnalysisWindows),
                    axis=1,
                )

            if not headerWritten:
                outputFile.write(";".join(columns) + "\n")
                headerWritten = True
            for row in result:
                outputFile.write(";".join(row) + "\n")

    case "VIB":
        columns = VIB_DEFAULT_FEATURES
        sensorInColumns = []

        vibFiles = list(
            pl.Path(os.path.join(PATH_RAW_DATA, setup)).glob(VIB_FILE_SEARCH_PATTERN)
        )

        currentTestFiles = []
        currentTestSzenario = ""
        for file in tqdm(vibFiles):
            testSzenario = os.path.dirname(file)

            if testSzenario != currentTestSzenario and len(currentTestFiles) > 0:
                sensorData = []
                for testFile in currentTestFiles:
                    testType = re.search(TEST_TYPE_REGEX, str(testFile)).group(0)
                    testName = re.search(TEST_NAME_REGEX, str(file)).group(0)

                    rpm = re.search(VIB_FILE_RPM_REGEX, str(testFile)).group(1)
                    flowRate = re.search(VIB_FILE_FLOW_RATE_REGEX, str(testFile)).group(
                        1
                    )
                    sensor = re.search(VIB_FILE_SENSOR_REGEX, testFile.name).group(1)

                    dataFrame = pd.read_csv(
                        testFile,
                        skiprows=2,
                        encoding="ISO-8859-1",
                        sep=";",
                        index_col=0,
                    )
                    dataFrame.index = pd.to_datetime(dataFrame.index, unit="ns")

                    while len(sensorData) < VIB_SENSOR_COUNT:
                        placeholder = pd.DataFrame(
                            0, index=dataFrame.index, columns=dataFrame.columns
                        )
                        placeholder.rename(
                            columns={"Value": "VIB" + str(len(sensorData) + 1)},
                            inplace=True,
                        )
                        sensorData.append(placeholder)

                    dataFrame.rename(
                        columns={"Value": "VIB" + str(sensor)}, inplace=True
                    )

                    sensorData[int(sensor) - 1] = dataFrame

                currentTestFiles = []

                mergedSensorData = sensorData[0]
                for i in range(1, len(sensorData)):
                    mergedSensorData = pd.merge_asof(
                        mergedSensorData,
                        sensorData[i],
                        left_index=True,
                        right_index=True,
                        tolerance=pd.Timedelta(str(windowSize // 100) + "ms"),
                    )

                resampled = mergedSensorData.resample(
                    str(windowSize) + "ms", origin="end"
                )

                median = resampled.median().to_numpy()
                mean = resampled.mean().to_numpy()
                max_val = resampled.max().to_numpy()
                min_val = resampled.min().to_numpy()
                std = resampled.std().to_numpy()

                range_val = max_val - min_val
                iqr = resampled.aggregate(
                    lambda sample: stats.iqr(sample.to_numpy())
                ).to_numpy()

                result = np.concatenate(
                    (
                        np.repeat(
                            [[testName, testType, flowRate, rpm]],
                            len(std),
                            axis=0,
                        ),
                        std,
                        range_val,
                        iqr,
                        median - mean,
                    ),
                    axis=1,
                )
                if frequencyAnalysis:
                    fftAnalysisWindows = []
                    for samples in resampled:
                        fftAnalysis = []
                        if samples[1].shape[0] >= (
                            maxFrequency * (windowSize / 1000) * 2
                        ):
                            for sensor in samples[1]:
                                sensorSamples = samples[1][sensor]
                                frequencyFeatures = fftFeatureExtraction(
                                    sensorSamples,
                                    sampleRate,
                                    maxFrequency,
                                    resampleCount,
                                    frequencyBandCount,
                                )
                                if not sensor in sensorInColumns:
                                    columns = np.concatenate(
                                        (
                                            columns,
                                            sensor + "_" + frequencyFeatures.index,
                                        )
                                    )
                                    sensorInColumns.append(sensor)
                                fftAnalysis.append(
                                    frequencyFeatures.to_numpy().flatten()
                                )
                        else:
                            fftAnalysis = [
                                np.array([0])
                                for i in range(
                                    samples[1].shape[1]
                                    * (frequencyBandCount * 4 + 6 + resampleCount * 5)
                                )
                            ]
                        fftAnalysisWindows.append(np.concatenate(fftAnalysis, axis=0))

                    result = np.concatenate(
                        (result, np.array(fftAnalysisWindows)),
                        axis=1,
                    )

                result = result[~np.any(result == "nan", axis=1)]

                if not headerWritten:
                    outputFile.write(";".join(columns) + "\n")
                    headerWritten = True
                for row in result:
                    outputFile.write(";".join(row) + "\n")

            currentTestSzenario = testSzenario
            currentTestFiles.append(file)

4.3 Vorbereitung der Tainings- und Testdaten¶

  • Vorverarbeitete Daten auswählen
  • Features wählen
  • Nach Spaltenwert filtern (z.B. Betriebspunkte oder Szenarien)
  • neuen Namen eingeben und speichern
In [ ]:
# SELECT PREPROCESSED DATA
clear_output(wait=True)
loading = False
featureDataDir = list(
    filter(
        lambda x: os.path.isfile(os.path.join(PATH_FEATURE_DATA, x)),
        os.listdir(PATH_FEATURE_DATA),
    )
)

preprocessedData = None
preprocessedFileDropdown = widgets.Dropdown(description="preprocessed data")
preprocessedFileDropdown.options = featureDataDir
display(preprocessedFileDropdown)

statusText = widgets.Label()
display(statusText)

# SELECT FEATURES
featureSelection = widgets.SelectMultiple(description="features")
display(featureSelection)

rowSelectorColumn = widgets.Dropdown(description="row selector column")
display(rowSelectorColumn)
rowSelection = widgets.SelectMultiple(description="rows")
display(rowSelection)


def updateDynamicFields():
    global featureSelection
    global rowSelection
    global rowSelectorColumn


    
    if (preprocessedData is None):
        featureSelection.options = []
        rowSelectorColumn.options = []
        rowSelection.options = []

        featureSelection.disabled = True
        rowSelection.disabled = True
        rowSelectorColumn.disabled = True
        if loading:
            statusText.value = "Lade Daten..."
        else:
            statusText.value = "Bitte Datei auswählen"
        return
    
    statusText.value = "Daten geladen"
    featureSelection.disabled = False
    rowSelection.disabled = False
    rowSelectorColumn.disabled = False

    featureSelection.options = list(preprocessedData.columns)
    featureSelection.value = featureSelection.options
    rowSelectorColumn.options = list(preprocessedData.columns)
    rowSelectorColumn.value = rowSelectorColumn.options[0]

    
    rowSelection.options = list(preprocessedData[rowSelectorColumn.value].unique())
    rowSelection.value = rowSelection.options
    

def rowSelectorColumnChange(change):
    global rowSelection
    global preprocessedData
    if loading:
        return
    rowSelection.options = list(preprocessedData[change.new].unique())
    rowSelection.value = rowSelection.options


def onPreprocessedFileChange(change):
    global preprocessedData
    global loading
    preprocessedData = None
    loading = True
    updateDynamicFields()
    preprocessedData = pd.read_csv(os.path.join(PATH_FEATURE_DATA, change.new), sep=";")
    loading = False
    updateDynamicFields()


preprocessedFileDropdown.observe(onPreprocessedFileChange, names="value")
rowSelectorColumn.observe(rowSelectorColumnChange, names="value")

saveName = widgets.Text(description="new name")
display(saveName)
saveButton = widgets.Button(description="save")
display(saveButton)

def onSaveButtonClick(b):
    global preprocessedData
    global featureSelection
    global rowSelection
    global saveName
    global rowSelectorColumn
    global saveButton
    saveButton.style.button_color = "lightblue"

    selectedFeatures = featureSelection.value
    selectedRows = rowSelection.value
    rowSelector = rowSelectorColumn.value

    preprocessedData = pd.read_csv(os.path.join(PATH_FEATURE_DATA, preprocessedFileDropdown.value), sep=";")

    if len(selectedFeatures) > 0 and len(selectedRows) > 0 and len(rowSelector) > 0:
        preprocessedData = preprocessedData[preprocessedData[rowSelector].isin(selectedRows)]
        preprocessedData = preprocessedData.loc[:, selectedFeatures]

        preprocessedData.to_csv(os.path.join(PATH_FEATURE_DATA, saveName.value + ".csv"), sep=";", index=False)
        saveButton.style.button_color = "lightgreen"


saveButton.on_click(onSaveButtonClick)
updateDynamicFields()

5. Maschinelles Lernen¶

In diesem Bereich können, mithilfe drei verschiedener Learner, Modell anhand der generierten Trainings- und Testdaten erzeugt werden.

5.1 Trainings- und Testdaten wählen und laden¶

In [ ]:
# SELECT TRAINING AND TEST DATA
clear_output(wait=True)
featureDataDir = list(filter(lambda x: os.path.isfile(os.path.join(PATH_FEATURE_DATA, x)), os.listdir(PATH_FEATURE_DATA)))

trainFileDropdown = widgets.Dropdown(description="training data")
trainFileDropdown.options = featureDataDir
display(trainFileDropdown)

testFileDropdown = widgets.Dropdown(description="test data")
testFileDropdown.options = featureDataDir
display(testFileDropdown)
In [ ]:
# LOAD DATA
csvTrain = read_csv(PATH_FEATURE_DATA + trainFileDropdown.value, delimiter=";")
csvTest = read_csv(PATH_FEATURE_DATA + testFileDropdown.value, delimiter=";")

DATA_INDEX_CLASS = 1
DATA_INDEX_TIME_BASED = 2
DATA_INDEX_FREQ_BASED = 12

trainData = csvTrain.values
testData = csvTest.values


featureNames = csvTrain.columns.values[DATA_INDEX_TIME_BASED:].tolist()
trainX, trainY = trainData[:, DATA_INDEX_TIME_BASED:].astype(np.float32), trainData[:, DATA_INDEX_CLASS:DATA_INDEX_CLASS+1]
testX, testY = testData[:, DATA_INDEX_TIME_BASED:].astype(np.float32), testData[:, DATA_INDEX_CLASS:DATA_INDEX_CLASS+1]

5.2 Entscheidungsbaum - sklearn (empfohlen)¶

In diesem Abschnitt wird mit der sklearn-Bibliothek ein Entscheidungsbaummodell trainiert. Dabei können verschiedene Parameter frei gewählt werden, wie die maximale Baumtiefe, die gewünschte Mindestgenauigkeit und die Anzahl der Suchiterationen. Das Programm wird dann versuchen, ausgehend von einer Baumtiefe von eins, einen möglichst einfachen Entscheidungsbaum zu generieren, der die gewünschte Genauigkeit erreicht.

5.2.1 Konfiguration 1¶

In [ ]:
DT_MAX_DEPTH = 8                           # Maximum depth of the tree
DT_EXPLORATION_TARGET_VAL_ACCURACY = 0.7   # Target accuracy for the decision tree
DT_EXPLORATION_MAX_ITER = 100              # Maximum number of iterations for the random search
DT_VERBOSE = True                         # Verbose logging for the decision tree

5.2.2 Trainieren 2¶

In [ ]:
# Train DecisionTreeClassifier
from matplotlib import pyplot as plt
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import LabelEncoder
from IPython.display import clear_output
import joblib

labelEncoder = LabelEncoder()
labelEncoder = labelEncoder.fit(np.ravel(trainY))
labelEncodedTrainY = labelEncoder.transform(np.ravel(trainY))
labelEncodedTestY = labelEncoder.transform(np.ravel(testY))


testAccuracy = 0.0
trainAccuracy = 0.0
iterations = 0
depth = 0

while (
    testAccuracy < DT_EXPLORATION_TARGET_VAL_ACCURACY
    or trainAccuracy < DT_EXPLORATION_TARGET_VAL_ACCURACY
) and iterations < DT_EXPLORATION_MAX_ITER:
    # Create decision tree classifier
    depth = 1 + int(DT_MAX_DEPTH * (iterations / DT_EXPLORATION_MAX_ITER) // 1)

    dt = DecisionTreeClassifier(max_depth=depth, splitter="random")
    dt.fit(trainX, labelEncodedTrainY)

    testPredictions = dt.predict(testX)
    testAccuracy = accuracy_score(labelEncodedTestY, testPredictions)

    trainPredictions = dt.predict(trainX)
    trainAccuracy = accuracy_score(labelEncodedTrainY, trainPredictions)
    iterations += 1

    if DT_VERBOSE:
        print("Iteration: %d" % iterations)
        print("Depth: %d" % depth)
        print("Train Accuracy: %.2f%%" % (trainAccuracy * 100.0))
        print("Test Accuracy: %.2f%%" % (testAccuracy * 100.0))

    


    


print("Iteration: %d" % iterations)
print("Depth: %d" % depth)
print("Train Accuracy: %.2f%%" % (trainAccuracy * 100.0))
print("Test Accuracy: %.2f%%" % (testAccuracy * 100.0))

modelName = "dtc." + str(round(time.time()))
os.makedirs(PATH_MODEL + modelName)
with open(PATH_MODEL + modelName + "/encoder.pickle", "wb") as f:
    joblib.dump(labelEncoder, f)
joblib.dump(dt, PATH_MODEL + modelName + "/dtc.model")

5.3 Tiefes neuronales Netz - tensorflow¶

Im folgenden Abschnitt wird mit der tensorflow-Bibliothek ein tiefes neuronales Netzwerk trainiert. Dabei können verschiedene Parameter eingestellt werden, wie die gewünschte Mindestgenauigkeit, die Anzahl der Suchiterationen, die minimale und maximale Anzahl an Schichten und Neuronen, die Toleranz für einen vorzeitigen Abbruch einer Iteration, die Ausgabeform, die maximale Anzahl an Epochen, die Batchgröße und die Batch Normalisierung.

Bitte beachte, dass die genaue Syntax und die verfügbaren Optionen von tensorflow abhängen können. Es ist empfehlenswert, die offizielle tensorflow-Dokumentation für detaillierte Informationen zu konsultieren.

5.3.1 Konfiguration 2¶

In [ ]:
DNN_EXPLORATION_TARGET_VAL_ACCURACY = 0.9  # Target accuracy for the neural network
DNN_EXPLORATION_MAX_ITER = 50              # Maximum number of iterations for the random search
DNN_EXPLORATION_HIDDEN_LAYERS_MIN = 1       # Minimum number of hidden layers
DNN_EXPLORATION_HIDDEN_LAYERS_MAX = 3       # Maximum number of hidden layers
DNN_EXPLORATION_NEURONS_MIN = 8             # Minimum number of neurons per layer
DNN_EXPLORATION_NEURONS_MAX = 32            # Maximum number of neurons per layer


DNN_EARLY_STOPPING_PATIENCE = 50            # Patience for early stopping
DNN_VERBOSE = 0                             # Verbosity level for the neural network
DNN_EPOCHS = 2000                           # Maximum number of epochs for the neural network
DNN_BATCH_SIZE = 128                        # Batch size for the neural network
DNN_BATCH_NORMALIZATION = True              # Batch normalization for the neural network

5.3.2 Trainieren 2¶

In [ ]:
# Train Neural Network
import json
import random
import joblib
from sklearn.preprocessing import OneHotEncoder
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense, InputLayer, BatchNormalization
from tensorflow.keras.callbacks import EarlyStopping

onehotencoder = OneHotEncoder()
onehotencoder = onehotencoder.fit(trainY)
onehotEncodedTrainY = onehotencoder.transform(trainY).toarray()
onehotEncodedTestY = onehotencoder.transform(testY).toarray()

explorationResults = []


numberOfFeatures = trainX.shape[1]
categorieCount = len(onehotEncodedTrainY[0])

testAccuracy = 0.0
trainAccuracy = 0.0
model = Sequential()
interation = 0
while (
    testAccuracy < DNN_EXPLORATION_TARGET_VAL_ACCURACY
    or trainAccuracy < DNN_EXPLORATION_TARGET_VAL_ACCURACY
) and interation < DNN_EXPLORATION_MAX_ITER:
    model = Sequential()
    model.add(InputLayer(input_shape=(numberOfFeatures,)))
    if DNN_BATCH_NORMALIZATION:
        model.add(BatchNormalization())
    denseCount = random.randint(
        DNN_EXPLORATION_HIDDEN_LAYERS_MIN, DNN_EXPLORATION_HIDDEN_LAYERS_MAX
    )
    denseNeurons = []
    for i in range(0, denseCount):
        neuronCount = random.randint(
            DNN_EXPLORATION_NEURONS_MIN, DNN_EXPLORATION_NEURONS_MAX
        )
        denseNeurons.append(neuronCount)
        model.add(Dense(neuronCount, activation="tanh"))
        if DNN_BATCH_NORMALIZATION:
            model.add(BatchNormalization())
    model.add(Dense(categorieCount, activation="sigmoid"))
    model.compile(
        optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]
    )
    model.fit(
        trainX,
        onehotEncodedTrainY,
        epochs=DNN_EPOCHS,
        batch_size=DNN_BATCH_SIZE,
        verbose=DNN_VERBOSE,
        validation_data=(testX, onehotEncodedTestY),
        callbacks=[
            EarlyStopping(
                monitor="val_loss",
                patience=DNN_EARLY_STOPPING_PATIENCE,
                restore_best_weights=True,
            )
        ],
    )

    trainLoss, trainAccuracy = model.evaluate(trainX, onehotEncodedTrainY, verbose=DNN_VERBOSE)
    testLoss, testAccuracy = model.evaluate(testX, onehotEncodedTestY, verbose=DNN_VERBOSE)

    explorationResults.append(
        {
            "dense_count": denseCount,
            "dense_neurons": denseNeurons,
            "train_loss": trainLoss,
            "train_acc": trainAccuracy,
            "test_loss": testLoss,
            "test_acc": testAccuracy,
        }
    )
    interation += 1

modelName = "dnn." + str(round(time.time()))

model.save(PATH_MODEL + modelName)
print("Iteration: %d" % interation)
print("Train Accuracy: %.2f%%" % (trainAccuracy * 100.0))
print("Test Accuracy: %.2f%%" % (testAccuracy * 100.0))

with open(PATH_MODEL + modelName + "/encoder.pickle", "wb") as f:
    joblib.dump(onehotencoder, f)

with open(PATH_EXPLORATION_DATA + modelName + ".exploration_results.json", "w") as f:
    json.dump(explorationResults, f, indent=4)

5.4 Extremes Gradienten-Boosting - XGBoost¶

In diesem Abschnitt wird mit der xgboost-Bibliothek ein Modell mithilfe des "Extreme Gradient Boosting" trainiert. Dabei können verschiedene Parameter frei gewählt werden, wie die maximale Anzahl der Bäume, die gewünschte Mindestgenauigkeit und die Anzahl der Suchiterationen.

5.4.1 Konfiguration 3¶

In [ ]:
DT_MAX_DEPTH = 16                           # Maximum depth of the tree
DT_EXPLORATION_TARGET_VAL_ACCURACY = 0.9    # Target accuracy for the decision tree
DT_EXPLORATION_MAX_ITER = 100               # Maximum number of iterations for the random search
DT_VERBOSE = False                          # Verbose logging for the decision tree
DT_NUM_OF_ESTIMATORS = 10                 # number of estimators (default = None -> number of estimators = number of classes)

5.4.2 Trainieren 3¶

In [ ]:
# Train XGBClassifier
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score
from xgboost import XGBClassifier
import joblib


labelEncoder = LabelEncoder()
labelEncoder = labelEncoder.fit(np.ravel(trainY))
labelEncodedTrainY = labelEncoder.transform(np.ravel(trainY))
labelEncodedTestY = labelEncoder.transform(np.ravel(testY))

testAccuracy = 0.0
trainAccuracy = 0.0
iterations = 0
while (
    testAccuracy < DT_EXPLORATION_TARGET_VAL_ACCURACY
    or trainAccuracy < DT_EXPLORATION_TARGET_VAL_ACCURACY
) and iterations < DT_EXPLORATION_MAX_ITER:
    xgb = XGBClassifier(
        tree_method="hist",
        enable_categorical=True,
        max_depth=DT_MAX_DEPTH,
        n_estimators=labelEncoder.classes_.size if DT_NUM_OF_ESTIMATORS == None else DT_NUM_OF_ESTIMATORS,
        random_state=np.random.randint(100000)
    )
    # fit model
    labeledTrainX = pd.DataFrame(trainX, columns=featureNames)
    labeledTestX = pd.DataFrame(testX, columns=featureNames)

    xgb.fit(
        labeledTrainX, labelEncodedTrainY, eval_set=[(labeledTestX, labelEncodedTestY)], verbose=1 if DT_VERBOSE else 0
    )

    testPredictions = xgb.predict(testX)
    testAccuracy = accuracy_score(labelEncodedTestY, testPredictions)

    trainPredictions = xgb.predict(trainX)
    trainAccuracy = accuracy_score(labelEncodedTrainY, trainPredictions)
    iterations += 1
    if DT_VERBOSE:
        print("Train Accuracy: %.2f%%" % (trainAccuracy * 100.0))
        print("Test Accuracy: %.2f%%" % (testAccuracy * 100.0))

modelName = "xgb." + str(round(time.time()))
os.makedirs(PATH_MODEL + modelName)

with open(PATH_MODEL + modelName + "/encoder.pickle", "wb") as f:
    joblib.dump(labelEncoder, f)


XGBClassifier.save_model(xgb, PATH_MODEL + modelName + "/xgb.model")

featureMap = xgb.get_booster().get_score(importance_type="gain")

with open(PATH_MODEL + modelName + "/feature_map.txt", "w") as file:
    for index, feature in enumerate(xgb.get_booster().feature_names):
        file.write(f"{index}\t{feature}\tq\n")


print("Iteration: %d" % iterations)
print("Train Accuracy: %.2f%%" % (trainAccuracy * 100.0))
print("Test Accuracy: %.2f%%" % (testAccuracy * 100.0))

6. Modelanalyse des Learners¶

In diesem Abschnitt können in Abhängigkeit von der Auswahl des zu analysierenden Modells verschiedene Analysen durchgeführt werden.

  • Visuelle Darstellung (nur Entscheidungsbaum/Extremes Gradienten-Boosting): Eine visuelle Darstellung des Modells kann erstellt werden, um die Entscheidungslogik und Struktur intuitiv zu erfassen.

  • Konfusionsmatrix: Eine Konfusionsmatrix wird erstellt, um die Leistung des Modells bei der Klassifikation zu bewerten. Sie zeigt, wie gut das Modell verschiedene Klassen korrekt vorhersagt und welche Fehler gemacht werden.

  • Test-Genauigkeit: Die Test-Genauigkeit wird berechnet, um zu bewerten, wie gut das Modell auf unbekannten Daten abschneidet. Dies gibt einen Indikator dafür, wie zuverlässig die Vorhersagen des Modells sind.

  • Feature Importances: Es wird eine Analyse der Feature Importances durchgeführt, um die relative Bedeutung der verschiedenen Merkmale bei der Vorhersage zu bestimmen. Dies ermöglicht Einblicke in die relevanten Merkmale und kann bei der Feature-Auswahl oder -Gewichtung helfen.

Diese Analysen bieten einen umfassenden Einblick in die Leistung und Funktionsweise des ausgewählten Modells und unterstützen bei der Interpretation der Ergebnisse.

In [ ]:
# SELECT LEARNER AND TEST DATA
clear_output(wait=True)
featureDataDir = list(filter(lambda x: os.path.isfile(os.path.join(PATH_FEATURE_DATA, x)), os.listdir(PATH_FEATURE_DATA)))
modelDir = os.listdir(PATH_MODEL)

modelDropdown = widgets.Dropdown(description="model")
modelDropdown.options = modelDir
display(modelDropdown)

testFileDropdown = widgets.Dropdown(description="test data")
testFileDropdown.options = featureDataDir
display(testFileDropdown)
In [ ]:
# ANALYZE MODEL
import tensorflow as tf
from tensorflow import math as tfmath
import tensorflow_probability as tfp
import eli5
from eli5.sklearn import PermutationImportance
import joblib
from matplotlib import pyplot
from xgboost import XGBClassifier, plot_tree
from sklearn import tree
from sklearn.metrics import accuracy_score

selectedTestFile = testFileDropdown.value
selectedModelFile = modelDropdown.value

print("Selected model: " + selectedModelFile)

csvTest = read_csv(PATH_FEATURE_DATA + selectedTestFile, delimiter=";")
testData = csvTest.values

testX, testY = testData[:,2:].astype(np.float32), testData[:, DATA_INDEX_CLASS:DATA_INDEX_CLASS+1]
featureNames = csvTest.columns.values[2:].tolist()

model = None
predictions = None
transformedTestY = None
confusionMatrix = None
classes = None
if (selectedModelFile.startswith('dnn')):
    onehotencoder = None
    with open(PATH_MODEL + selectedModelFile + '/encoder.pickle', 'rb') as f:
        onehotencoder = joblib.load(f)
    classes = onehotencoder.categories_[0]
    transformedTestY = onehotencoder.transform(testY).toarray()    

    model = tf.keras.models.load_model(PATH_MODEL + selectedModelFile)
    predictions = model.predict(testX)

    confusionMatrix = tf.math.confusion_matrix(np.argmax(transformedTestY, axis=1), np.argmax(predictions, axis=1))
    equality = tf.math.equal(np.argmax(predictions, axis=1), np.argmax(transformedTestY, axis=1))
    accuracy = tf.math.reduce_mean(tf.cast(equality, tf.float32))
elif (selectedModelFile.startswith('xgb')):
    labelEncoder = None
    with open(PATH_MODEL + selectedModelFile + '/encoder.pickle', 'rb') as f:
        labelEncoder = joblib.load(f)
    classes = labelEncoder.classes_
    transformedTestY = labelEncoder.transform(np.ravel(testY))

    model = XGBClassifier()
    model.load_model(PATH_MODEL + selectedModelFile + '/xgb.model')
    predictions = model.predict(testX)
    confusionMatrix = tfmath.confusion_matrix(transformedTestY, predictions)

    for i in range(model.n_estimators):
        plot_tree(model, num_trees=i, fmap=PATH_MODEL + selectedModelFile + '/feature_map.txt')
        pyplot.gcf().set_dpi(1200)
        pyplot.show()


    #pyplot.show()
    equality = tf.math.equal(predictions, transformedTestY)
    accuracy = tf.math.reduce_mean(tf.cast(equality, tf.float32))
elif (selectedModelFile.startswith('dtc')):
    labelEncoder = None
    with open(PATH_MODEL + selectedModelFile + '/encoder.pickle', 'rb') as f:
        labelEncoder = joblib.load(f)
    classes = labelEncoder.classes_
    transformedTestY = labelEncoder.transform(np.ravel(testY))

    model = joblib.load(PATH_MODEL + selectedModelFile + '/dtc.model')

    predictions = model.predict(testX)
    accuracy = accuracy_score(transformedTestY, predictions)
    confusionMatrix = tfmath.confusion_matrix(transformedTestY, predictions)
    pyplot.figure(figsize=(120, 40))       
    tree.plot_tree(model, feature_names=featureNames, class_names=labelEncoder.classes_, filled=True)
    pyplot.show()

mat = pyplot.matshow(confusionMatrix, 1)
mat.axes.set_xticks(np.arange(0, len(classes), 1))
mat.axes.set_yticks(np.arange(0, len(classes), 1))
mat.axes.set_xticklabels(classes, rotation=90)
mat.axes.set_yticklabels(classes)
for (x, y), value in np.ndenumerate(confusionMatrix):
    pyplot.text(y, x, f"{value:.2f}", va="center", ha="center")
pyplot.show()


print('Test Accuracy: %.3f' % accuracy)


#correlationMatrix = tfp.stats.correlation(testX)
#pyplot.matshow(correlationMatrix)
#pyplot.show()

perm = PermutationImportance(model, scoring="neg_mean_squared_error", random_state=1).fit(testX, transformedTestY)
print(eli5.format_as_text(eli5.explain_weights(perm, feature_names=featureNames)))

7. Statische Interpretation des Resultats¶

7.1 Deskriptive und Explorative Datenanalyse¶

7.1.1 Aggregation¶

Die deskriptive Datenanalyse der Gutdaten ergab interessante Erkenntnisse. Eine Fehlausrichtung zeigt sich durch die Umkehrung von p1 und p2. Dieser Effekt wird besonders deutlich, wenn der maximale Förderstrom (100%) betrachtet wird. In diesem Fall wird die Fehlausrichtung sehr gut sichtbar.

KiDAQ Visualisierung

Ein Laufradschaden hingegen äußert sich durch eine vergrößerte Entfernung zwischen p1 und p2. Besonders auffällig ist, dass sich dieser Abstand auch beim maximalen Förderstrom (100%) weiter vergrößert. Es zeigt sich also ein Trend, dass sowohl p1 als auch p2 sich voneinander entfernen, welches auf einen möglichen Laufradschaden hinweisen könnte.

Das Resultat der Untersuchung ist, dass Veränderungen der Daten bereits anhand der unterschiedlichen Messreihen erkannt werden können. Sowohl die Fehlausrichtung, als auch der Laufradschaden lassen sich durch die Analyse der p1- und p2-Werte identifizieren. Dies ermöglicht eine frühzeitige Erkennung von potenziellen Problemen und gibt die Möglichkeit, geeignete Maßnahmen zur Behebung einzuleiten, bevor schwerwiegendere Schäden auftreten.

Die deskriptive Datenanalyse liefert somit wertvolle Informationen, um die Qualität der Daten zu beurteilen und potenzielle Abweichungen oder Schäden zu erkennen. Durch regelmäßige Überprüfung der Messreihen können frühzeitig Anomalien festgestellt und entsprechende Maßnahmen ergriffen werden, um die Funktionalität und Zuverlässigkeit der untersuchten Systeme aufrechtzuerhalten.

7.1.2 Frequenztransformation¶

7.1.2.1 FFT der Vibrationssignale von KiDAQ und VIB im Vergleich¶

Die Auswertung der Vibrationssignale mithilfe der Fast-Fourier-Transformation (FFT) ergab interessante Unterschiede zwischen dem Kidaq-System und dem Vib-System bezüglich des Frequenzverhaltens.

Im KiDAQ-System liegen die meisten Werte im niedrigen Frequenzbereich. Dies deutet darauf hin, dass die dominanten Frequenzen oder Frequenzbereiche der Vibrationssignale im Kidaq-System hauptsächlich im niedrigen Bereich zu finden sind. Dies kann auf eine bestimmte Art von Vibrationen oder Schwingungen hinweisen, die in diesem System häufig auftreten.

KiDAQ FFT KiDAQ FFT

Im VIB-System hingegen sind die meisten Werte gleichmäßig im Frequenzbereich zwischen 0 und 1000 verteilt. Dies deutet darauf hin, dass im VIB-System eine breitere Palette von Frequenzen in den Vibrationssignalen vorhanden ist. Es gibt jedoch immer noch einen signifikaten Anstieg im Frequenzbereich von 800 bis 1000, was darauf hindeuten kann, dass in diesem Frequenzbereich eine erhöhte Aktivität oder eine spezifische physikalische Eigenschaft vorliegt.

VIB FFT

Eine grobe Interpretation dieser Ergebnisse könnte darauf hinweisen, dass das KiDAQ-System möglicherweise empfindlicher oder anfälliger für niedrigfrequente Vibrationen ist, während das VIB-System eine breitere Bandbreite von Frequenzen erfasst, einschließlich höherfrequenter Vibrationen. Der Anstieg im Bereich von 800 bis 1000 im Vib-System könnte auf eine spezifische resonante Eigenschaft oder ein bestimmtes Verhalten bestimmter Komponenten hinweisen.

Die FFT-Analyse liefert somit wertvolle Informationen über das Frequenzverhalten der Vibrationssignale und kann bei der Identifizierung von Mustern oder Anomalien helfen. Durch die Analyse der Frequenzspektren können bestimmte Frequenzen oder Frequenzbereiche identifiziert werden, die auf bestimmte Eigenschaften oder Verhaltensweisen hinweisen können. Dies ermöglicht es, die Vibrationssignale besser zu verstehen und die Leistung der Systeme zu verbessern. Diese Eigenschaften wurde ebenfalls für das Training des künstlich neuronalen Netzes verwendet, welche im Abschnitt 5. Maschinelles Learning erläutert wurde.

7.1.2.2 Verwendung und Interpretation von unterschiedlichen spektralen Analysemethoden¶

Innerhalb des Kaptiels 3. Deskriptive und explorative Datenanalyse wurden verschiedene spektrale Analysemethoden vorgestellt. Diese wurden innerhalb des Projektes immer weiter verfeinert und um neue Analysemethoden erweitert. Zu Beginn des Projekts wurde mit einer sehr einfach gehaltenen vollständig selbst implementierten FFT-Funktion gestartet, welche allerdings nicht mit den verschiedenen Datenformaten von KiDAQ, VIB und vor allem zwischen dem Setup-I und Setup-II harmonisierte und nicht das gewünschte Ergebnis lieferte. In einem zweiten Durchlauf wurde diese Funktion mithilfe von NumPys fft-Methode ersetzt und das Ergebnis hat sich nach ein paar Durchläufen deutlich verbessert. Es war nun möglich verschiedene FFT-Plots zu erzeugen. Das Ergebnis wurde iterativ immer wieder mit den bereits bestehenden Ergebnissen aus dem Scientific Paper "Vergleichende Untersuchung einer schwingungs-und druckbasierten Condition Monitoring Anwendung am Beispiel einer Normkreiselpumpe" [vgl. ResearchGate, M. Ladwig] verglichen. Nach unterschiedlichen Einstellungen und Anpassungen der Parameter war es nun möglich die Ergebnisse zu reproduzieren. Um die Vibrationsdaten nun in einem höheren Detailgrad auswerten zu können, ist ein klassische FFT-Analyse nicht mehr ausreichend. Die klassische Fast-Fourier-Transformation ist ein hervorragendes Werkzeug zur Analyse der Frequenzkomponenten von Signalen, insbesondere wenn es sich um ein stationäres Signal handelt, das über die gesamte Dauer gleich bleibt. Allerdings haben sie ihre Grenzen, wenn es um die Analyse von Signalen geht, die zeitlich variabel sind oder schnelle Veränderungen enthalten. Hier können die Short-Time-Fourier-Transformation und/oder die Wavelet-Transformation abhilfe schaffen. Beide bieten verschiedene Vorteile:

  • Short-Time Fourier Transformation: Die STFT ist eine Erweiterung der klassischen Fourier-Transformation, die das Signal in kleinere überlappende Zeitabschnitte (Fenster) aufteilt und in jedem Fenster eine FFT durchführt. Dadurch erhalten wir Informationen über die zeitlichen Veränderungen der Frequenzkomponenten eines Signals. STFTs sind besonders nützlich, wenn sich die Signalcharakteristik im Laufe der Zeit ändert. Durch die Visualisierung der STFT kann man spektrogrammartige Darstellungen erzeugen. Die Intensität der Frequenzkomponenten wird durch Farbe oder Helligkeit dargestellen. Vor allem der Aspekt der zeitlichen Veränderung der Frequenzkomponenten ist für die Analyse von Vibrationsdaten sehr interessant und bietet zusätzliche Informationen.
  • Wavelet-Transformation: Wavelet-Transformation ist ähnlich wie die der STFT, aber sie verwenden spezielle Wavelet-Funktionen anstelle von fixen Fenstern. Die Wavelet-Transformation bietet eine höhere Lokalisierung in Zeit und Frequenz, was sie besonders nützlich für die Analyse von Signalen mit nicht-stationären Merkmalen macht. Wavelets erlauben es, zeitliche Ereignisse mit variabler Dauer zu identifizieren und zu lokalisieren, was in vielen Anwendungen von großem Vorteil ist. Beispielsweise kann man mit Wavelets scharfe und gut lokalisierte Peaks in der Signalanalyse erkennen, die mit klassischen FFTs möglicherweise verloren gehen würden.

Um dieses Aussage zu bestätigen, wurden die unterschiedlichen Analysemethoden auf die Vibrationsdaten von KiDAQ angewendet und die Ergebnisse nebeneinander gestellt. Als Referenz wurde das Setup-I mit der Versuchsreihe des Laufradschadens 1 (vom 10.07.2021) mit 1450rpm bei 100% Durchfluss gewählt. Die Ergebnisse der unterschiedlichen Analysemethoden sind in den folgenden Abbildungen dargestellt:

FFT p1 STFT p1 Wavelet p1
FFT p1 STFT p1 Wavelet p1

Aus dem FFT-Plot kann man sehr einfach die Frequenzzusammensetzung eines Signals erkennen. Zusätzlich werden die Frequenzkomponenten in der Amplitude dargestellt. Allerdings ist es nicht möglich die zeitliche Veränderung der Frequenzkomponenten zu erkennen. Dies ist nur durch die STFT und die Wavelet-Transformation möglich. Der STFT-Plot zeigt die zeitliche Veränderung der Frequenzkomponenten. Hingegen ist es nicht möglich die Amplitude der Frequenzkomponenten zu erkennen. Dies ist nur durch die FFT und die Wavelet-Transformation möglich. Der Wavelet-Plot zeigt die zeitliche Veränderung der Frequenzkomponenten und die Amplitude der Frequenzkomponenten. Allerdings ist es nicht möglich die Frequenzkomponenten zu erkennen. Dies ist nur durch die FFT und die STFT möglich. Es wird also deutlich, dass die unterschiedlichen Analysemethoden unterschiedliche Vorteile bieten. Um die Vibrationsdaten in einem höheren Detailgrad auswerten zu können, ist es notwendig die unterschiedlichen Analysemethoden zu kombinieren, um einen deutlich besseren Einblick in die Daten zu erhalten und die Ergebnisse zu validieren.

Auf Basis dessen entstand eine weitere Analysefunktion mit dem Name "FFT - Time". Innerhalb dieser Funktion sollten alle drei Analysemethoden kombiniert werden, um die Vorteile der einzelnen Methoden zu nutzen. Die Funktion nutzt als Grundlage die Idee des STFT bietet aber zusätzlich den Parameter der step-size, um so das Resampling der Daten selbstständig zu wählen und über den Slider anzupassen. Es wurde wieder das Setup-I mit der Versuchsreihe des Laufradschadens 1 (vom 10.07.2021) mit 1450rpm bei 100% Durchfluss gewählt. Ferner wurde die maximal Frequenz auf 1000Hz gesetzt, um den Einfluss der step-size besser zu erkennen. Die Ergebnisse der Funktion sind in den folgenden Abbildungen dargestellt:

FFT p1 FFT p1
FFT p1 100 FFT p2 100
FFT p1 250 FFT p2 250
FFT p1 500 FFT p2 500
FFT p1 750 FFT p2 750
FFT p1 1000 FFT p2 1000

Es wird schnell deutlich, dass die Step-Size einen großen Einfluss auf die Aussagekraft der Visualisierung hat und die Ergebnisse deutlich besser interpretiert werden können. Während bei einer Step-Size von 100 die einzelnen Frequenzkomponenten gut zu erkennen sind, ist es bei einer step-size von 1000 nicht mehr möglich die Frequenzkomponenten zu erkennen. Es ist also notwendig die Step-Size an die Daten anzupassen, um eine gute Visualisierung zu erhalten. Vor allem die Ergebnisse der Step-Size von 100 und 250 sind sehr interessant, da hier die Frequenzkomponenten sehr gut zu erkennen sind und die zeitliche Veränderung der Frequenzkomponenten sehr gut zu erkennen ist. Es ist also möglich die Frequenzkomponenten zu erkennen und die zeitliche Veränderung der Frequenzkomponenten zu erkennen. Dies ist mit den klassischen FFTs nicht möglich.

7.2 Interpretation des maschinellen Lernens¶

7.2.1 VIB¶

Durch die Anwendung einfacher aggregativer Mittel kann bereits eine annähernd perfekte Klassifikation der Betriebsarten anhand der VIB-Daten erreicht werden. Dabei ist sowohl die Genauigkeit hoch als auch die Modellkomplexität gering. Ein Entscheidungsbaum erzielt bereits mit einer Tiefe von 6 eine Genauigkeit von über 95%. Diese Kennzahlen sind sowohl innerhalb eines Setups reproduzierbar als auch bei der Anwendung eines Modells, das mit einem anderen Setup trainiert wurde. Die Anwendung einer Frequenzanalyse und die Übermittlung entsprechender Frequenzmerkmale an den Lernalgorithmus führen zu keiner Verbesserung.

VIB DTREE

Setup 1¶

VIB ConfMat S1
Permutation Importance:

Bezeichnung Wert Unsicherheit
s1_std 1.0526 0.0191
s5_range 0.7759 0.0201
s3_mean_median 0.6220 0.0309
s3_iqr 0.6117 0.0488
rpm 0.2643 0.0164
s2_std 0.2600 0.0327
s8_mean_median 0.2339 0.0205
s7_iqr 0.1605 0.0122
s4_iqr 0.1429 0.0140
s3_std 0.1195 0.0112
s5_iqr 0.1185 0.0015
s2_iqr 0.0968 0.0070
s7_std 0.0782 0.0046
s5_mean_median 0.0726 0.0314

Setup 2¶

VIB ConfMat S2
Permutation Importance:

Bezeichnung Wert Unsicherheit
s5_iqr 0.4961 0.0073
s5_range 0.0030 0.0005
s1_std 0.0018 0.0020
s8_std 0.0012 0.0000
s3_iqr 0.0010 0.0002
s7_iqr 0.0010 0.0014
s2_range 0.0005 0.0000
s7_range 0.0000 0.0000
s3_std 0.0000 0.0000
s4_std 0.0000 0.0000
s5_std 0.0000 0.0000
s6_std 0.0000 0.0000
s7_std 0.0000 0.0000
s3_range 0.0000 0.0000

7.2.2 KiDAQ¶

Der Versuch, ein betriebspunktunabhängiges Modell mithilfe der KiDAQ-Daten zu trainieren, gestaltet sich deutlich schwieriger. Während im ersten Setup noch eine Genauigkeit von über 70% erreicht werden kann, sieht es bei der Validierung mit Hilfe von Setup 2 leider deutlich schlechter aus. Dort scheint eine Konfusion zwischen Laufradschaden, Fehlausrichtung und Gutfällen zu bestehen. Entweder liegt hier ein Fehler in den Daten vor oder die Fälle lassen sich nicht unabhängig vom Setup differenzieren.

Im Allgemeinen ist der Datenbestand quantitativ zu gering, um eine endgültige Aussage über die Klassifizierbarkeit der Fehler zu treffen.

VIB DTREE

Setup 1¶

KiDAQ ConfMat S1
Permutation Importance:

Bezeichnung Wert Unsicherheit
p1_band_325.0_349.0_energy_mean 0.3924 0.0029
p2_275_300_std 0.3769 0.0087
p1_band_400.0_424.0_energy 0.3656 0.0085
p1_band_450.0_474.0_energy_mean 0.3247 0.0015
p2_band_250.0_274.0_energy 0.2490 0.0063
p2_75_100_range 0.2276 0.0032
p2_band_275.0_299.0_energy 0.2098 0.0073
p1_std 0.1274 0.0025
p2_150_175_std 0.1150 0.0023
p2_band_300.0_324.0_energy_mean 0.1093 0.0050
p2_amplitude_19_highest_energy 0.1080 0.0046
p1_band_300.0_324.0_energy 0.0933 0.0022
rpm 0.0778 0.0030
p2_100_125_std 0.0770 0.0051

Setup 2¶

KiDAQ ConfMat S2
Permutation Importance:

Bezeichnung Wert Unsicherheit
p1_std 0.8039 0.0422
p1_band_225.0_249.0_energy 0.3475 0.0217
p2_band_275.0_299.0_energy 0.0862 0.0075
p2_band_400.0_424.0_energy 0.0841 0.0171
p2_band_325.0_349.0_energy_mean 0.0101 0.0021
p1_amplitude_13_highest_energy 0.0006 0.0010
p1_amplitude_14_highest_energy 0 0
p1_amplitude_15_highest_energy 0 0
p1_amplitude_16_highest_energy 0 0
p1_amplitude_17_highest_energy 0 0
p1_amplitude_1_highest_freq 0 0
p1_amplitude_20_highest_energy 0 0
p1_amplitude_12_highest_energy 0 0
p1_amplitude_2_highest_freq 0 0